Files
LookingGlass/client/src/clipboard_files.c
2026-08-15 20:29:08 +10:00

3740 lines
104 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
*/
#define FUSE_USE_VERSION 310
#include "core/clipboard_files.h"
#include "core/clipboard.h"
#include "common/KVMFRClipboard.h"
#include "common/debug.h"
#include "common/event.h"
#include "common/locking.h"
#include "common/thread.h"
#include "common/time.h"
#include <fuse3/fuse_lowlevel.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <limits.h>
#include <poll.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/eventfd.h>
#include <sys/random.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sys/types.h>
#include <unistd.h>
#define FILE_WORKERS 4U
#define FILE_ATTR_TIMEOUT 1.0
#define FILE_ENTRY_TIMEOUT 1.0
#define FILE_REQUEST_MAX KVMFR_CLIPBOARD_FILE_READ_BYTES
#define FILE_STREAM_CHUNK KVMFR_CLIPBOARD_DATA_BYTES
#define FILE_DELIVERY_GRACE_US UINT64_C(30000000)
#define FILE_DELIVERY_GRACE_MAX 4U
#define FILE_URI_PREFIX "file://"
#define FILE_GNOME_MIME "x-special/gnome-copied-files"
#define FILE_MATE_MIME "x-special/mate-copied-files"
#define FILE_URI_MIME "text/uri-list"
#define FILE_KDE_CUT_MIME "application/x-kde-cutselection"
_Static_assert(FILE_REQUEST_MAX <= UINT32_MAX,
"clipboard FUSE read size exceeds the FUSE protocol field");
enum RemoteReply
{
REMOTE_REPLY_INITIAL,
REMOTE_REPLY_LOOKUP,
REMOTE_REPLY_READDIR,
REMOTE_REPLY_READ,
};
struct RemoteDataset;
struct RemoteNode
{
struct RemoteNode * next;
struct RemoteDataset * dataset;
fuse_ino_t ino;
fuse_ino_t parent;
uint64_t node;
uint64_t size;
uint64_t createdNs;
uint64_t modifiedNs;
KVMFRClipboardFileType type;
uint64_t lookups;
bool listed;
char * name;
};
struct RemoteDataset
{
struct RemoteDataset * next;
uint64_t dataset;
uint64_t acquisition;
fuse_ino_t ino;
uint64_t lookupPins;
uint64_t openPins;
uint64_t requestPins;
uint64_t operationPins;
uint64_t deliveryDeadline;
unsigned presentationPins;
bool acquiring;
bool acquired;
bool ready;
bool current;
bool stale;
bool deliveryPending;
bool pruneQueued;
char name[40];
};
struct RemoteRequest
{
struct RemoteRequest * next;
LG_ClipboardFileRequest request;
struct RemoteDataset * dataset;
struct RemoteNode * directory;
enum RemoteReply reply;
fuse_req_t fuseRequest;
size_t fuseSize;
off_t fuseOffset;
char * lookupName;
uint8_t * data;
size_t dataSize;
size_t dataCapacity;
uint64_t sizeHint;
bool began;
bool transportStarted;
};
enum RemoteActionType
{
REMOTE_ACTION_NONE,
REMOTE_ACTION_CANCEL_ACQUISITION,
REMOTE_ACTION_RELEASE_ACQUISITION,
};
struct RemoteAction
{
enum RemoteActionType type;
struct RemoteDataset * object;
uint64_t dataset;
uint64_t acquisition;
};
struct LocalDataset;
struct LocalNode
{
struct LocalNode * next;
struct LocalDataset * dataset;
struct LocalNode * root;
uint64_t node;
uint64_t parent;
uint64_t size;
uint64_t createdNs;
uint64_t modifiedNs;
KVMFRClipboardFileType type;
dev_t device;
ino_t inode;
int fd;
char * path;
char * name;
};
struct LocalDataset
{
struct LocalDataset * next;
struct LocalNode * nodes;
uint64_t nodeSerial;
uint64_t dataset;
unsigned references;
bool current;
};
struct LocalAcquisition
{
struct LocalAcquisition * next;
struct LocalDataset * dataset;
uint64_t wireDataset;
uint64_t acquisition;
};
struct LocalJob
{
struct LocalJob * next;
struct LocalDataset * dataset;
LG_ClipboardFileRequest request;
LGEvent * ready;
atomic_bool cancelled;
};
static struct
{
LG_Lock lock;
struct fuse_session * session;
LGThread * fuseThread;
int fuseStopFd;
char mountpoint[PATH_MAX];
bool initialized;
bool stopping;
fuse_ino_t inodeSerial;
uint64_t requestSerial;
uint64_t datasetSerial;
struct RemoteDataset * remoteDatasets;
struct RemoteDataset * remoteCurrent;
struct RemoteNode * remoteNodes;
struct RemoteRequest * remoteRequests;
struct LocalDataset * localDatasets;
struct LocalDataset * localCurrent;
struct LocalAcquisition * localAcquisitions;
struct LocalJob * jobsHead;
struct LocalJob ** jobsTail;
struct LocalJob * activeJobs;
LGEvent * jobEvent;
LGThread * workers[FILE_WORKERS];
}
files;
static bool filesLockInitialized;
#ifdef ENABLE_TESTS
static atomic_bool forceLocalEof;
#endif
static bool randomBytes(void * buffer, size_t size)
{
uint8_t * bytes = buffer;
size_t offset = 0;
while (offset < size)
{
const ssize_t result = getrandom(bytes + offset, size - offset, 0);
if (result < 0 && errno == EINTR)
continue;
if (result <= 0)
{
if (!result)
errno = EIO;
return false;
}
offset += (size_t)result;
}
return true;
}
static uint64_t nsFromTimespec(struct timespec value)
{
if (value.tv_sec < 0 || value.tv_nsec < 0)
return 0;
if ((uint64_t)value.tv_sec > UINT64_MAX / UINT64_C(1000000000))
return UINT64_MAX;
return (uint64_t)value.tv_sec * UINT64_C(1000000000) +
(uint64_t)value.tv_nsec;
}
static struct timespec timespecFromNs(uint64_t value)
{
return (struct timespec)
{
.tv_sec = (time_t)(value / UINT64_C(1000000000)),
.tv_nsec = (long)(value % UINT64_C(1000000000)),
};
}
static uint64_t nextRequestNL(void)
{
do
++files.requestSerial;
while (!files.requestSerial ||
kvmfrClipboardTransferFromHelper(files.requestSerial));
return files.requestSerial;
}
static uint64_t nextAcquisitionNL(void)
{
return nextRequestNL();
}
static uint64_t nextDatasetNL(void)
{
do
++files.datasetSerial;
while (!files.datasetSerial ||
kvmfrClipboardTransferFromHelper(files.datasetSerial));
return files.datasetSerial;
}
static fuse_ino_t nextInodeNL(void)
{
if (++files.inodeSerial <= FUSE_ROOT_ID)
files.inodeSerial = FUSE_ROOT_ID + 1;
return files.inodeSerial;
}
static int errnoFromFileError(LG_ClipboardFileError error)
{
switch (error)
{
case LG_CLIPBOARD_FILE_ERROR_NONE: return 0;
case LG_CLIPBOARD_FILE_ERROR_NOT_FOUND: return ENOENT;
case LG_CLIPBOARD_FILE_ERROR_ACCESS: return EACCES;
case LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY: return ENOTDIR;
case LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY: return EISDIR;
case LG_CLIPBOARD_FILE_ERROR_NO_MEMORY: return ENOMEM;
case LG_CLIPBOARD_FILE_ERROR_NO_SPACE: return ENOSPC;
case LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED: return ENOTSUP;
case LG_CLIPBOARD_FILE_ERROR_CANCELLED: return EINTR;
case LG_CLIPBOARD_FILE_ERROR_STALE: return ESTALE;
default: return EIO;
}
}
static const char * fileErrorName(LG_ClipboardFileError error)
{
switch (error)
{
case LG_CLIPBOARD_FILE_ERROR_NONE: return "none";
case LG_CLIPBOARD_FILE_ERROR_NOT_FOUND: return "not found";
case LG_CLIPBOARD_FILE_ERROR_ACCESS: return "access denied";
case LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY: return "not a directory";
case LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY: return "is a directory";
case LG_CLIPBOARD_FILE_ERROR_IO: return "I/O error";
case LG_CLIPBOARD_FILE_ERROR_INVALID: return "invalid";
case LG_CLIPBOARD_FILE_ERROR_NO_MEMORY: return "out of memory";
case LG_CLIPBOARD_FILE_ERROR_NO_SPACE: return "no space";
case LG_CLIPBOARD_FILE_ERROR_DISCONNECTED: return "disconnected";
case LG_CLIPBOARD_FILE_ERROR_CANCELLED: return "cancelled";
case LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED: return "not supported";
case LG_CLIPBOARD_FILE_ERROR_STALE: return "stale";
}
return "unknown";
}
static LG_ClipboardFileError fileErrorFromErrno(int error)
{
switch (error)
{
case ENOENT: return LG_CLIPBOARD_FILE_ERROR_NOT_FOUND;
case EACCES:
case EPERM: return LG_CLIPBOARD_FILE_ERROR_ACCESS;
case ENOTDIR: return LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY;
case EISDIR: return LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY;
case ENOMEM:
case EMFILE:
case ENFILE: return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
case ENOSPC: return LG_CLIPBOARD_FILE_ERROR_NO_SPACE;
case ELOOP:
case ENOTSUP: return LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED;
case ESTALE: return LG_CLIPBOARD_FILE_ERROR_STALE;
case ECANCELED: return LG_CLIPBOARD_FILE_ERROR_CANCELLED;
default: return LG_CLIPBOARD_FILE_ERROR_IO;
}
}
static bool grow(void ** buffer, size_t * capacity, size_t wanted)
{
if (wanted <= *capacity)
return !wanted || *buffer;
size_t next = *capacity ? *capacity : 4096U;
while (next < wanted)
{
if (next > SIZE_MAX / 2U)
{
next = wanted;
break;
}
next *= 2U;
}
void * resized = realloc(*buffer, next);
if (!resized)
return false;
*buffer = resized;
*capacity = next;
return true;
}
static struct RemoteDataset * remoteDatasetByIdNL(uint64_t dataset)
{
for (struct RemoteDataset * item = files.remoteDatasets;
item; item = item->next)
if (item->dataset == dataset)
return item;
return NULL;
}
static struct RemoteDataset * remoteDatasetByAcquisitionNL(
uint64_t acquisition)
{
for (struct RemoteDataset * item = files.remoteDatasets;
item; item = item->next)
if (item->acquisition == acquisition)
return item;
return NULL;
}
static struct RemoteAction retireRemoteDatasetNL(
struct RemoteDataset * dataset)
{
struct RemoteAction action = { 0 };
if (!dataset || dataset->current || dataset->presentationPins ||
dataset->lookupPins || dataset->openPins || dataset->requestPins ||
dataset->operationPins || dataset->deliveryPending)
return action;
if (!dataset->stale)
{
dataset->ready = false;
dataset->stale = true;
action.dataset = dataset->dataset;
action.acquisition = dataset->acquisition;
if (dataset->acquired)
action.type = REMOTE_ACTION_RELEASE_ACQUISITION;
else if (dataset->acquiring)
action.type = REMOTE_ACTION_CANCEL_ACQUISITION;
dataset->acquiring = false;
dataset->acquired = false;
}
if (dataset->pruneQueued)
return action;
dataset->pruneQueued = true;
action.object = dataset;
return action;
}
/* Retire a failed initial acquisition/list only while it is still the
* dataset backing the current remote file offer. The caller must hold
* files.lock and subsequently call retireRemoteDatasetNL so an acquired
* dataset is released before it is pruned. */
static uint64_t failCurrentRemoteDatasetNL(
struct RemoteDataset * dataset)
{
if (!dataset || !dataset->current)
return 0;
dataset->current = false;
dataset->ready = false;
dataset->deliveryPending = false;
dataset->deliveryDeadline = 0;
if (files.remoteCurrent == dataset)
files.remoteCurrent = NULL;
return dataset->dataset;
}
static void pruneRemoteDataset(struct RemoteDataset * dataset)
{
if (!dataset)
return;
LG_LOCK(files.lock);
if (!dataset->pruneQueued || !dataset->stale || dataset->current ||
dataset->presentationPins || dataset->lookupPins ||
dataset->openPins || dataset->requestPins ||
dataset->operationPins || dataset->deliveryPending)
{
dataset->pruneQueued = false;
LG_UNLOCK(files.lock);
return;
}
struct RemoteDataset ** datasetLink = &files.remoteDatasets;
while (*datasetLink && *datasetLink != dataset)
datasetLink = &(*datasetLink)->next;
if (!*datasetLink)
{
LG_UNLOCK(files.lock);
return;
}
*datasetLink = dataset->next;
if (files.remoteCurrent == dataset)
files.remoteCurrent = NULL;
struct RemoteNode * retired = NULL;
struct RemoteNode ** nodeLink = &files.remoteNodes;
while (*nodeLink)
{
struct RemoteNode * node = *nodeLink;
if (node->dataset != dataset)
{
nodeLink = &node->next;
continue;
}
*nodeLink = node->next;
node->next = retired;
retired = node;
}
LG_UNLOCK(files.lock);
while (retired)
{
struct RemoteNode * next = retired->next;
free(retired->name);
free(retired);
retired = next;
}
free(dataset);
}
static void performRemoteAction(struct RemoteAction action)
{
switch (action.type)
{
case REMOTE_ACTION_CANCEL_ACQUISITION:
if (!clipboard_fileCancel(action.dataset, action.acquisition,
LG_CLIPBOARD_FILE_ERROR_CANCELLED))
DEBUG_WARN("Failed to cancel retired clipboard file acquisition");
break;
case REMOTE_ACTION_RELEASE_ACQUISITION:
if (!clipboard_fileRelease(action.dataset, action.acquisition))
DEBUG_WARN("Failed to release retired clipboard file acquisition");
break;
case REMOTE_ACTION_NONE:
break;
}
pruneRemoteDataset(action.object);
}
static bool pinRemoteOperationNL(
struct RemoteDataset * dataset, bool consumeDelivery)
{
if (!dataset || dataset->stale)
return false;
++dataset->operationPins;
if (consumeDelivery)
{
dataset->deliveryPending = false;
dataset->deliveryDeadline = 0;
}
return true;
}
static void releaseRemoteOperation(struct RemoteDataset * dataset)
{
if (!dataset)
return;
LG_LOCK(files.lock);
if (dataset->operationPins)
--dataset->operationPins;
const struct RemoteAction action = retireRemoteDatasetNL(dataset);
LG_UNLOCK(files.lock);
performRemoteAction(action);
}
static void reapRemoteDeliveries(bool pressure)
{
for (;;)
{
const uint64_t now = microtime();
LG_LOCK(files.lock);
struct RemoteDataset * candidate = NULL;
struct RemoteDataset * oldest = NULL;
unsigned pending = 0;
for (struct RemoteDataset * dataset = files.remoteDatasets;
dataset; dataset = dataset->next)
{
if (!dataset->deliveryPending)
continue;
if (dataset->deliveryDeadline <= now)
{
candidate = dataset;
break;
}
if (dataset->current)
continue;
++pending;
if (!oldest || dataset->deliveryDeadline < oldest->deliveryDeadline)
oldest = dataset;
}
if (!candidate && pressure && pending > FILE_DELIVERY_GRACE_MAX)
candidate = oldest;
if (!candidate)
{
LG_UNLOCK(files.lock);
return;
}
candidate->deliveryPending = false;
candidate->deliveryDeadline = 0;
const struct RemoteAction action = retireRemoteDatasetNL(candidate);
LG_UNLOCK(files.lock);
performRemoteAction(action);
}
}
static int nextRemoteDeliveryTimeout(void)
{
const uint64_t now = microtime();
uint64_t deadline = UINT64_MAX;
LG_LOCK(files.lock);
for (struct RemoteDataset * dataset = files.remoteDatasets;
dataset; dataset = dataset->next)
if (dataset->deliveryPending &&
dataset->deliveryDeadline < deadline)
deadline = dataset->deliveryDeadline;
LG_UNLOCK(files.lock);
if (deadline == UINT64_MAX)
return -1;
if (deadline <= now)
return 0;
const uint64_t delta = deadline - now;
const uint64_t milliseconds = delta / UINT64_C(1000) +
(delta % UINT64_C(1000) != 0);
return milliseconds > INT_MAX ? INT_MAX : (int)milliseconds;
}
static void pruneUnusedRemoteDatasets(void)
{
for (;;)
{
LG_LOCK(files.lock);
struct RemoteDataset * dataset = files.remoteDatasets;
while (dataset && (dataset->current || dataset->presentationPins ||
dataset->lookupPins || dataset->openPins || dataset->requestPins ||
dataset->operationPins || dataset->deliveryPending ||
dataset->pruneQueued))
dataset = dataset->next;
const struct RemoteAction action = retireRemoteDatasetNL(dataset);
LG_UNLOCK(files.lock);
if (!action.object)
return;
performRemoteAction(action);
}
}
static struct RemoteNode * remoteNodeByInodeNL(fuse_ino_t ino)
{
for (struct RemoteNode * node = files.remoteNodes; node; node = node->next)
if (node->ino == ino)
return node;
return NULL;
}
static struct RemoteNode * remoteChildNL(struct RemoteDataset * dataset,
fuse_ino_t parent, const char * name)
{
for (struct RemoteNode * node = files.remoteNodes; node; node = node->next)
if (node->dataset == dataset && node->parent == parent &&
!strcmp(node->name, name))
return node;
return NULL;
}
static struct RemoteNode * remoteNodeByWireNL(
struct RemoteDataset * dataset, uint64_t wireNode)
{
for (struct RemoteNode * node = files.remoteNodes; node; node = node->next)
if (node->dataset == dataset && node->node == wireNode)
return node;
return NULL;
}
static bool validUtf8(const uint8_t * value, size_t length)
{
for (size_t i = 0; i < length;)
{
const uint8_t first = value[i++];
if (first < 0x80)
continue;
unsigned continuation;
uint32_t codepoint;
uint32_t minimum;
if ((first & 0xe0U) == 0xc0U)
{
continuation = 1;
codepoint = first & 0x1fU;
minimum = 0x80;
}
else if ((first & 0xf0U) == 0xe0U)
{
continuation = 2;
codepoint = first & 0x0fU;
minimum = 0x800;
}
else if ((first & 0xf8U) == 0xf0U)
{
continuation = 3;
codepoint = first & 0x07U;
minimum = 0x10000;
}
else
return false;
if (continuation > length - i)
return false;
while (continuation--)
{
const uint8_t next = value[i++];
if ((next & 0xc0U) != 0x80U)
return false;
codepoint = (codepoint << 6) | (next & 0x3fU);
}
if (codepoint < minimum || codepoint > 0x10ffffU ||
(codepoint >= 0xd800U && codepoint <= 0xdfffU))
return false;
}
return true;
}
static struct RemoteRequest * remoteRequestTakeNL(uint64_t request)
{
struct RemoteRequest ** link = &files.remoteRequests;
while (*link && (*link)->request.request != request)
link = &(*link)->next;
if (!*link)
return NULL;
struct RemoteRequest * result = *link;
*link = result->next;
result->next = NULL;
return result;
}
static struct RemoteRequest * remoteRequestTakePointerNL(
struct RemoteRequest * request)
{
struct RemoteRequest ** link = &files.remoteRequests;
while (*link && *link != request)
link = &(*link)->next;
if (!*link)
return NULL;
*link = request->next;
request->next = NULL;
return request;
}
static struct RemoteAction completeRemoteRequestNL(
struct RemoteRequest * request)
{
if (request && request->dataset->requestPins)
--request->dataset->requestPins;
return request ? retireRemoteDatasetNL(request->dataset) :
(struct RemoteAction) { 0 };
}
static void freeRemoteRequest(struct RemoteRequest * request)
{
if (!request)
return;
free(request->lookupName);
free(request->data);
free(request);
}
static void unregisterRemoteInterrupt(struct RemoteRequest * request)
{
if (request && request->fuseRequest)
fuse_req_interrupt_func(request->fuseRequest, NULL, NULL);
}
static void failRemoteRequest(struct RemoteRequest * request, int error)
{
if (!request)
return;
if (request->reply == REMOTE_REPLY_INITIAL)
DEBUG_WARN("Remote clipboard file root list failed: "
"dataset=%lu, request=%lu, error=%d (%s)",
request->request.dataset, request->request.request,
error, strerror(error));
unregisterRemoteInterrupt(request);
if (request->fuseRequest)
fuse_reply_err(request->fuseRequest, error);
LG_LOCK(files.lock);
const uint64_t failedDataset = request->reply == REMOTE_REPLY_INITIAL ?
failCurrentRemoteDatasetNL(request->dataset) : 0;
const struct RemoteAction action = completeRemoteRequestNL(request);
LG_UNLOCK(files.lock);
freeRemoteRequest(request);
performRemoteAction(action);
if (failedDataset)
clipboard_fileRemoteFailed(failedDataset);
}
static void completeRemoteRequest(struct RemoteRequest * request)
{
LG_LOCK(files.lock);
const struct RemoteAction action = completeRemoteRequestNL(request);
LG_UNLOCK(files.lock);
freeRemoteRequest(request);
performRemoteAction(action);
}
static void fillStat(const struct RemoteNode * node, struct stat * value)
{
memset(value, 0, sizeof(*value));
value->st_ino = node->ino;
value->st_mode = node->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ?
S_IFDIR | 0500 : S_IFREG | 0400;
value->st_nlink = node->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? 2 : 1;
value->st_uid = getuid();
value->st_gid = getgid();
value->st_size = node->size > INT64_MAX ? INT64_MAX : (off_t)node->size;
value->st_mtim = timespecFromNs(node->modifiedNs);
value->st_ctim = value->st_mtim;
value->st_atim = value->st_mtim;
}
static void replyEntry(fuse_req_t request, struct RemoteNode * node)
{
struct fuse_entry_param entry =
{
.ino = node->ino,
.generation = 1,
.attr_timeout = FILE_ATTR_TIMEOUT,
.entry_timeout = FILE_ENTRY_TIMEOUT,
};
fillStat(node, &entry.attr);
LG_LOCK(files.lock);
const bool available = !node->dataset->stale;
if (available)
{
++node->lookups;
++node->dataset->lookupPins;
}
LG_UNLOCK(files.lock);
if (!available)
{
fuse_reply_err(request, ESTALE);
return;
}
if (fuse_reply_entry(request, &entry) == 0)
return;
LG_LOCK(files.lock);
if (node->lookups)
--node->lookups;
if (node->dataset->lookupPins)
--node->dataset->lookupPins;
const struct RemoteAction action = retireRemoteDatasetNL(node->dataset);
LG_UNLOCK(files.lock);
performRemoteAction(action);
}
static bool installRemoteListNL(struct RemoteRequest * request)
{
struct RemoteNode * additions = NULL;
size_t offset = 0;
while (offset < request->dataSize)
{
if (request->dataSize - offset < sizeof(KVMFRClipboardFileEntry))
goto invalid;
KVMFRClipboardFileEntry entry;
memcpy(&entry, request->data + offset, sizeof(entry));
if (!entry.node || !entry.nameLength || entry.nameLength > NAME_MAX ||
(entry.type != KVMFR_CLIPBOARD_FILE_TYPE_REGULAR &&
entry.type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY))
goto invalid;
const uint64_t bytes64 = KVMFR_CLIPBOARD_FILE_ENTRY_BYTES(
entry.nameLength);
if (bytes64 > SIZE_MAX || bytes64 > request->dataSize - offset)
goto invalid;
const uint8_t * encodedName = request->data + offset +
sizeof(entry);
if (memchr(encodedName, 0, entry.nameLength) ||
!validUtf8(encodedName, entry.nameLength) ||
(entry.nameLength == 1 && encodedName[0] == '.') ||
(entry.nameLength == 2 && encodedName[0] == '.' &&
encodedName[1] == '.') ||
memchr(encodedName, '/', entry.nameLength))
goto invalid;
for (size_t i = sizeof(entry) + entry.nameLength;
i < (size_t)bytes64; ++i)
if (request->data[offset + i])
goto invalid;
char * name = malloc((size_t)entry.nameLength + 1U);
if (!name)
goto invalid;
memcpy(name, encodedName, entry.nameLength);
name[entry.nameLength] = 0;
struct RemoteNode * existing = remoteChildNL(
request->dataset, request->directory->ino, name);
struct RemoteNode * wireNode = remoteNodeByWireNL(
request->dataset, entry.node);
for (struct RemoteNode * node = additions; node; node = node->next)
{
if (node->parent == request->directory->ino &&
!strcmp(node->name, name))
existing = node;
if (node->node == entry.node)
wireNode = node;
}
if (existing || wireNode)
{
const bool same = existing && existing == wireNode &&
existing->size == entry.size &&
existing->createdNs == entry.createdNs &&
existing->modifiedNs == entry.modifiedNs &&
existing->type == entry.type;
free(name);
if (!same)
goto invalid;
}
else
{
struct RemoteNode * node = calloc(1, sizeof(*node));
if (!node)
{
free(name);
goto invalid;
}
*node = (struct RemoteNode)
{
.dataset = request->dataset,
.ino = nextInodeNL(),
.parent = request->directory->ino,
.node = entry.node,
.size = entry.size,
.createdNs = entry.createdNs,
.modifiedNs = entry.modifiedNs,
.type = entry.type,
.name = name,
.next = additions,
};
additions = node;
}
offset += (size_t)bytes64;
}
while (additions)
{
struct RemoteNode * node = additions;
additions = node->next;
node->next = files.remoteNodes;
files.remoteNodes = node;
}
request->directory->listed = true;
return true;
invalid:
while (additions)
{
struct RemoteNode * next = additions->next;
free(additions->name);
free(additions);
additions = next;
}
return false;
}
static void replyDirectory(struct RemoteRequest * request)
{
char * buffer = malloc(request->fuseSize ? request->fuseSize : 1U);
if (!buffer)
{
fuse_reply_err(request->fuseRequest, ENOMEM);
return;
}
size_t used = 0;
off_t index = 0;
struct stat attr;
if (request->fuseOffset <= index)
{
fillStat(request->directory, &attr);
const size_t entry = fuse_add_direntry(request->fuseRequest,
buffer + used, request->fuseSize - used, ".", &attr, index + 1);
if (entry > request->fuseSize - used)
goto done;
used += entry;
}
++index;
if (request->fuseOffset <= index)
{
LG_LOCK(files.lock);
struct RemoteNode * parent = remoteNodeByInodeNL(
request->directory->parent);
fillStat(parent ? parent : request->directory, &attr);
LG_UNLOCK(files.lock);
const size_t entry = fuse_add_direntry(request->fuseRequest,
buffer + used, request->fuseSize - used, "..", &attr, index + 1);
if (entry > request->fuseSize - used)
goto done;
used += entry;
}
LG_LOCK(files.lock);
for (struct RemoteNode * node = files.remoteNodes; node; node = node->next)
{
if (node->dataset != request->dataset ||
node->parent != request->directory->ino)
continue;
++index;
if (request->fuseOffset > index)
continue;
fillStat(node, &attr);
const size_t entry = fuse_add_direntry(request->fuseRequest,
buffer + used, request->fuseSize - used,
node->name, &attr, index + 1);
if (entry > request->fuseSize - used)
break;
used += entry;
}
LG_UNLOCK(files.lock);
done:
fuse_reply_buf(request->fuseRequest, buffer, used);
free(buffer);
}
static void finishRemoteRequest(struct RemoteRequest * request)
{
unregisterRemoteInterrupt(request);
bool installed = true;
if (request->request.operation == LG_CLIPBOARD_FILE_LIST)
{
LG_LOCK(files.lock);
installed = installRemoteListNL(request);
if (installed && request->reply == REMOTE_REPLY_INITIAL)
{
request->dataset->ready = true;
const bool current = request->dataset->current;
const uint64_t datasetId = request->dataset->dataset;
const uint64_t requestId = request->request.request;
LG_UNLOCK(files.lock);
if (current)
{
DEBUG_INFO("Remote clipboard file root list ready; publishing: "
"dataset=%lu, request=%lu",
datasetId, requestId);
clipboard_fileRemoteReady(datasetId);
}
completeRemoteRequest(request);
return;
}
if (installed && request->reply == REMOTE_REPLY_LOOKUP)
{
struct RemoteNode * node = remoteChildNL(request->dataset,
request->directory->ino, request->lookupName);
LG_UNLOCK(files.lock);
if (node)
replyEntry(request->fuseRequest, node);
else
fuse_reply_err(request->fuseRequest, ENOENT);
completeRemoteRequest(request);
return;
}
LG_UNLOCK(files.lock);
if (!installed && request->reply == REMOTE_REPLY_INITIAL)
{
failRemoteRequest(request, EIO);
return;
}
if (installed && request->reply == REMOTE_REPLY_READDIR)
replyDirectory(request);
else if (request->fuseRequest)
fuse_reply_err(request->fuseRequest, EIO);
}
else if (request->reply == REMOTE_REPLY_READ)
{
if (request->dataSize > request->request.length)
fuse_reply_err(request->fuseRequest, EIO);
else
fuse_reply_buf(request->fuseRequest,
(const char *)request->data, request->dataSize);
}
completeRemoteRequest(request);
}
static void interruptRemoteRequest(fuse_req_t fuseRequest, void * opaque)
{
struct RemoteRequest * request = opaque;
LG_LOCK(files.lock);
request = remoteRequestTakePointerNL(request);
const struct RemoteAction action = completeRemoteRequestNL(request);
LG_UNLOCK(files.lock);
if (!request)
return;
const LG_ClipboardFileRequest descriptor = request->request;
if (request->transportStarted &&
!clipboard_fileCancel(descriptor.dataset, descriptor.request,
LG_CLIPBOARD_FILE_ERROR_CANCELLED))
DEBUG_WARN("Failed to cancel interrupted clipboard file request");
fuse_reply_err(fuseRequest, EINTR);
freeRemoteRequest(request);
performRemoteAction(action);
}
/* Returns true when another path already took ownership of the request. */
static bool finishUnsentRemoteRequest(uint64_t requestId)
{
LG_LOCK(files.lock);
struct RemoteRequest * failed = remoteRequestTakeNL(requestId);
LG_UNLOCK(files.lock);
if (!failed)
return true;
failRemoteRequest(failed, EIO);
return false;
}
static bool requestRemote(struct RemoteDataset * dataset,
struct RemoteNode * node, LG_ClipboardFileOperation operation,
uint64_t offset, uint32_t length, enum RemoteReply reply,
fuse_req_t fuseRequest, size_t fuseSize, off_t fuseOffset,
const char * lookupName)
{
LG_LOCK(files.lock);
const bool stale = dataset->stale;
LG_UNLOCK(files.lock);
if (stale)
return false;
struct RemoteRequest * request = calloc(1, sizeof(*request));
if (!request)
return false;
if (lookupName && !(request->lookupName = strdup(lookupName)))
{
free(request);
return false;
}
LG_LOCK(files.lock);
if (dataset->stale ||
(reply == REMOTE_REPLY_INITIAL && !dataset->current))
{
LG_UNLOCK(files.lock);
freeRemoteRequest(request);
return false;
}
request->request = (LG_ClipboardFileRequest)
{
.dataset = dataset->dataset,
.request = nextRequestNL(),
.node = node->node,
.offset = offset,
.length = length,
.operation = operation,
};
request->dataset = dataset;
request->directory = node;
request->reply = reply;
request->fuseRequest = fuseRequest;
request->fuseSize = fuseSize;
request->fuseOffset = fuseOffset;
request->sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN;
request->next = files.remoteRequests;
files.remoteRequests = request;
++dataset->requestPins;
LG_UNLOCK(files.lock);
const LG_ClipboardFileRequest descriptor = request->request;
if (fuseRequest)
fuse_req_interrupt_func(fuseRequest, interruptRemoteRequest, request);
LG_LOCK(files.lock);
struct RemoteRequest * live = files.remoteRequests;
while (live && live != request)
live = live->next;
const bool sent = live && clipboard_fileRequest(&descriptor);
if (sent)
request->transportStarted = true;
LG_UNLOCK(files.lock);
if (!live)
return true;
if (sent)
return true;
return finishUnsentRemoteRequest(descriptor.request);
}
static struct RemoteNode * datasetRootNL(struct RemoteDataset * dataset)
{
return remoteNodeByInodeNL(dataset->ino);
}
static void fuseLookup(fuse_req_t request, fuse_ino_t parent,
const char * name)
{
LG_LOCK(files.lock);
if (parent == FUSE_ROOT_ID)
{
for (struct RemoteDataset * dataset = files.remoteDatasets;
dataset; dataset = dataset->next)
if (dataset->ready && !dataset->stale &&
!strcmp(dataset->name, name))
{
struct RemoteNode * node = datasetRootNL(dataset);
const bool pinned = pinRemoteOperationNL(dataset, true);
LG_UNLOCK(files.lock);
if (pinned)
replyEntry(request, node);
else
fuse_reply_err(request, ESTALE);
releaseRemoteOperation(pinned ? dataset : NULL);
return;
}
LG_UNLOCK(files.lock);
fuse_reply_err(request, ENOENT);
return;
}
struct RemoteNode * directory = remoteNodeByInodeNL(parent);
const int error = !directory ? ENOENT :
directory->dataset->stale ? ESTALE :
directory->type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? ENOTDIR : 0;
if (error)
{
LG_UNLOCK(files.lock);
fuse_reply_err(request, error);
return;
}
struct RemoteNode * child = remoteChildNL(directory->dataset, parent, name);
const bool listed = directory->listed;
struct RemoteDataset * dataset = directory->dataset;
const bool pinned = pinRemoteOperationNL(dataset, false);
LG_UNLOCK(files.lock);
if (!pinned)
{
fuse_reply_err(request, ESTALE);
return;
}
if (child)
{
replyEntry(request, child);
releaseRemoteOperation(dataset);
return;
}
if (listed)
{
fuse_reply_err(request, ENOENT);
releaseRemoteOperation(dataset);
return;
}
if (!requestRemote(dataset, directory,
LG_CLIPBOARD_FILE_LIST, 0, 0, REMOTE_REPLY_LOOKUP,
request, 0, 0, name))
fuse_reply_err(request, EIO);
releaseRemoteOperation(dataset);
}
static void fillRootStat(struct stat * value)
{
memset(value, 0, sizeof(*value));
value->st_ino = FUSE_ROOT_ID;
value->st_mode = S_IFDIR | 0500;
value->st_nlink = 2;
value->st_uid = getuid();
value->st_gid = getgid();
}
static void fuseGetattr(fuse_req_t request, fuse_ino_t ino,
struct fuse_file_info * info)
{
(void)info;
struct stat value;
if (ino == FUSE_ROOT_ID)
fillRootStat(&value);
else
{
LG_LOCK(files.lock);
struct RemoteNode * node = remoteNodeByInodeNL(ino);
if (node && !node->dataset->stale)
fillStat(node, &value);
const bool stale = node && node->dataset->stale;
const bool found = node && !stale;
LG_UNLOCK(files.lock);
if (!found)
{
fuse_reply_err(request, stale ? ESTALE : ENOENT);
return;
}
}
fuse_reply_attr(request, &value, FILE_ATTR_TIMEOUT);
}
static void fuseOpendir(fuse_req_t request, fuse_ino_t ino,
struct fuse_file_info * info)
{
struct RemoteDataset * dataset = NULL;
if (ino != FUSE_ROOT_ID)
{
LG_LOCK(files.lock);
struct RemoteNode * node = remoteNodeByInodeNL(ino);
const int error = !node ? ENOENT :
node->dataset->stale ? ESTALE :
node->type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? ENOTDIR : 0;
if (!error)
{
dataset = node->dataset;
++dataset->openPins;
info->fh = dataset->acquisition;
}
LG_UNLOCK(files.lock);
if (error)
{
fuse_reply_err(request, error);
return;
}
}
if (fuse_reply_open(request, info) == 0 || !dataset)
return;
LG_LOCK(files.lock);
if (dataset->openPins)
--dataset->openPins;
const struct RemoteAction action = retireRemoteDatasetNL(dataset);
LG_UNLOCK(files.lock);
performRemoteAction(action);
}
static void replyRootDirectory(fuse_req_t request, size_t size, off_t offset)
{
char * buffer = malloc(size ? size : 1U);
if (!buffer)
{
fuse_reply_err(request, ENOMEM);
return;
}
size_t used = 0;
off_t index = 0;
struct stat attr;
fillRootStat(&attr);
const char * fixed[] = { ".", ".." };
for (unsigned i = 0; i < 2; ++i, ++index)
{
if (offset > index)
continue;
const size_t entry = fuse_add_direntry(request, buffer + used,
size - used, fixed[i], &attr, index + 1);
if (entry > size - used)
goto done;
used += entry;
}
LG_LOCK(files.lock);
for (struct RemoteDataset * dataset = files.remoteDatasets;
dataset; dataset = dataset->next)
{
if (!dataset->ready || dataset->stale)
continue;
++index;
if (offset > index)
continue;
struct RemoteNode * node = datasetRootNL(dataset);
fillStat(node, &attr);
const size_t entry = fuse_add_direntry(request, buffer + used,
size - used, dataset->name, &attr, index + 1);
if (entry > size - used)
break;
used += entry;
}
LG_UNLOCK(files.lock);
done:
fuse_reply_buf(request, buffer, used);
free(buffer);
}
static void fuseReaddir(fuse_req_t request, fuse_ino_t ino,
size_t size, off_t offset, struct fuse_file_info * info)
{
if (ino == FUSE_ROOT_ID)
{
replyRootDirectory(request, size, offset);
return;
}
LG_LOCK(files.lock);
struct RemoteNode * directory = remoteNodeByInodeNL(ino);
const int error = !directory ? ENOENT :
directory->dataset->stale ||
info->fh != directory->dataset->acquisition ? ESTALE :
directory->type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? ENOTDIR : 0;
if (error)
{
LG_UNLOCK(files.lock);
fuse_reply_err(request, error);
return;
}
const bool listed = directory->listed;
struct RemoteDataset * dataset = directory->dataset;
const bool pinned = pinRemoteOperationNL(dataset, false);
LG_UNLOCK(files.lock);
if (!pinned)
{
fuse_reply_err(request, ESTALE);
return;
}
if (listed)
{
struct RemoteRequest local =
{
.dataset = dataset,
.directory = directory,
.fuseRequest = request,
.fuseSize = size,
.fuseOffset = offset,
};
replyDirectory(&local);
releaseRemoteOperation(dataset);
return;
}
if (!requestRemote(dataset, directory,
LG_CLIPBOARD_FILE_LIST, 0, 0, REMOTE_REPLY_READDIR,
request, size, offset, NULL))
fuse_reply_err(request, EIO);
releaseRemoteOperation(dataset);
}
static void fuseOpen(fuse_req_t request, fuse_ino_t ino,
struct fuse_file_info * info)
{
LG_LOCK(files.lock);
struct RemoteNode * node = remoteNodeByInodeNL(ino);
const int error = !node ? ENOENT :
node->dataset->stale ? ESTALE :
node->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? EISDIR : 0;
if (error)
{
LG_UNLOCK(files.lock);
fuse_reply_err(request, error);
return;
}
if ((info->flags & O_ACCMODE) != O_RDONLY)
{
LG_UNLOCK(files.lock);
fuse_reply_err(request, EROFS);
return;
}
struct RemoteDataset * dataset = node->dataset;
++dataset->openPins;
info->fh = dataset->acquisition;
LG_UNLOCK(files.lock);
info->keep_cache = 1;
if (fuse_reply_open(request, info) == 0)
return;
LG_LOCK(files.lock);
if (dataset->openPins)
--dataset->openPins;
const struct RemoteAction action = retireRemoteDatasetNL(dataset);
LG_UNLOCK(files.lock);
performRemoteAction(action);
}
static void fuseRead(fuse_req_t request, fuse_ino_t ino, size_t size,
off_t offset, struct fuse_file_info * info)
{
if (offset < 0)
{
fuse_reply_err(request, EINVAL);
return;
}
LG_LOCK(files.lock);
struct RemoteNode * node = remoteNodeByInodeNL(ino);
const int error = !node ? ENOENT :
node->dataset->stale ||
info->fh != node->dataset->acquisition ? ESTALE :
node->type != KVMFR_CLIPBOARD_FILE_TYPE_REGULAR ? EISDIR : 0;
if (error)
{
LG_UNLOCK(files.lock);
fuse_reply_err(request, error);
return;
}
const uint64_t fileSize = node->size;
struct RemoteDataset * dataset = node->dataset;
const bool pinned = pinRemoteOperationNL(dataset, false);
LG_UNLOCK(files.lock);
if (!pinned)
{
fuse_reply_err(request, ESTALE);
return;
}
if ((uint64_t)offset >= fileSize || !size)
{
fuse_reply_buf(request, NULL, 0);
releaseRemoteOperation(dataset);
return;
}
uint64_t wanted = fileSize - (uint64_t)offset;
if (wanted > size)
wanted = size;
if (wanted > FILE_REQUEST_MAX)
wanted = FILE_REQUEST_MAX;
if (!requestRemote(dataset, node, LG_CLIPBOARD_FILE_READ,
(uint64_t)offset, (uint32_t)wanted, REMOTE_REPLY_READ,
request, 0, 0, NULL))
fuse_reply_err(request, EIO);
releaseRemoteOperation(dataset);
}
static void fuseStatfs(fuse_req_t request, fuse_ino_t ino)
{
(void)ino;
struct statvfs value;
memset(&value, 0, sizeof(value));
value.f_bsize = 4096;
value.f_frsize = 4096;
value.f_namemax = NAME_MAX;
value.f_flag = ST_RDONLY | ST_NOSUID;
fuse_reply_statfs(request, &value);
}
static void fuseAccess(fuse_req_t request, fuse_ino_t ino, int mask)
{
if (mask & W_OK)
{
fuse_reply_err(request, EROFS);
return;
}
if (ino == FUSE_ROOT_ID)
{
fuse_reply_err(request, 0);
return;
}
LG_LOCK(files.lock);
struct RemoteNode * node = remoteNodeByInodeNL(ino);
const bool found = node && !node->dataset->stale;
const bool stale = node && node->dataset->stale;
LG_UNLOCK(files.lock);
fuse_reply_err(request, found ? 0 : stale ? ESTALE : ENOENT);
}
static void forgetRemoteNode(fuse_ino_t ino, uint64_t count)
{
LG_LOCK(files.lock);
struct RemoteNode * node = remoteNodeByInodeNL(ino);
struct RemoteAction action = { 0 };
if (node)
{
if (count > node->lookups)
count = node->lookups;
node->lookups -= count;
if (count > node->dataset->lookupPins)
node->dataset->lookupPins = 0;
else
node->dataset->lookupPins -= count;
action = retireRemoteDatasetNL(node->dataset);
}
LG_UNLOCK(files.lock);
performRemoteAction(action);
}
static void fuseForget(fuse_req_t request, fuse_ino_t ino,
uint64_t count)
{
if (ino != FUSE_ROOT_ID)
forgetRemoteNode(ino, count);
fuse_reply_none(request);
}
static void fuseForgetMulti(fuse_req_t request, size_t count,
struct fuse_forget_data * forgets)
{
for (size_t i = 0; i < count; ++i)
if (forgets[i].ino != FUSE_ROOT_ID)
forgetRemoteNode(forgets[i].ino, forgets[i].nlookup);
fuse_reply_none(request);
}
static void releaseRemoteHandle(fuse_req_t request,
struct fuse_file_info * info)
{
if (!info->fh)
{
fuse_reply_err(request, 0);
return;
}
LG_LOCK(files.lock);
struct RemoteDataset * dataset = remoteDatasetByAcquisitionNL(info->fh);
const bool found = dataset != NULL;
if (dataset && dataset->openPins)
--dataset->openPins;
const struct RemoteAction action = retireRemoteDatasetNL(dataset);
LG_UNLOCK(files.lock);
fuse_reply_err(request, found ? 0 : ESTALE);
performRemoteAction(action);
}
static void fuseRelease(fuse_req_t request, fuse_ino_t ino,
struct fuse_file_info * info)
{
(void)ino;
releaseRemoteHandle(request, info);
}
static void fuseReleasedir(fuse_req_t request, fuse_ino_t ino,
struct fuse_file_info * info)
{
(void)ino;
releaseRemoteHandle(request, info);
}
static void fuseInit(void * userData, struct fuse_conn_info * connection)
{
(void)userData;
connection->max_read = FILE_REQUEST_MAX;
}
static const struct fuse_lowlevel_ops fuseOps =
{
.init = fuseInit,
.lookup = fuseLookup,
.forget = fuseForget,
.forget_multi = fuseForgetMulti,
.getattr = fuseGetattr,
.opendir = fuseOpendir,
.readdir = fuseReaddir,
.releasedir = fuseReleasedir,
.open = fuseOpen,
.read = fuseRead,
.release = fuseRelease,
.access = fuseAccess,
.statfs = fuseStatfs,
};
enum FuseWaitResult
{
FUSE_WAIT_ERROR,
FUSE_WAIT_SESSION,
FUSE_WAIT_WAKE,
FUSE_WAIT_TIMEOUT,
};
static enum FuseWaitResult waitFuseEvent(
int sessionFd, int stopFd, int timeout)
{
struct pollfd descriptors[] =
{
{ .fd = sessionFd, .events = POLLIN },
{ .fd = stopFd, .events = POLLIN },
};
int result;
do
result = poll(descriptors,
sizeof(descriptors) / sizeof(*descriptors), timeout);
while (result < 0 && errno == EINTR);
if (result < 0)
return FUSE_WAIT_ERROR;
if (!result)
return FUSE_WAIT_TIMEOUT;
if (descriptors[1].revents & POLLIN)
{
uint64_t value;
ssize_t received;
do
received = read(stopFd, &value, sizeof(value));
while (received < 0 && errno == EINTR);
if (received != (ssize_t)sizeof(value) &&
!(received < 0 && errno == EAGAIN))
return FUSE_WAIT_ERROR;
return FUSE_WAIT_WAKE;
}
if (descriptors[1].revents)
{
errno = EIO;
return FUSE_WAIT_ERROR;
}
if (descriptors[0].revents & POLLIN)
return FUSE_WAIT_SESSION;
errno = EIO;
return FUSE_WAIT_ERROR;
}
static bool wakeFuseThread(int stopFd)
{
const uint64_t value = 1;
ssize_t result;
do
result = write(stopFd, &value, sizeof(value));
while (result < 0 && errno == EINTR);
return result == (ssize_t)sizeof(value) ||
(result < 0 && errno == EAGAIN);
}
static int fuseThread(void * opaque)
{
struct fuse_session * session = opaque;
struct fuse_buf buffer =
{
.mem = NULL,
};
int result = 0;
while (!fuse_session_exited(session))
{
switch (waitFuseEvent(fuse_session_fd(session), files.fuseStopFd,
nextRemoteDeliveryTimeout()))
{
case FUSE_WAIT_WAKE:
{
LG_LOCK(files.lock);
const bool stopping = files.stopping;
LG_UNLOCK(files.lock);
if (stopping)
goto done;
reapRemoteDeliveries(false);
continue;
}
case FUSE_WAIT_TIMEOUT:
reapRemoteDeliveries(false);
continue;
case FUSE_WAIT_ERROR:
result = -errno;
goto done;
case FUSE_WAIT_SESSION:
break;
}
result = fuse_session_receive_buf(session, &buffer);
if (result == -EINTR)
continue;
if (result <= 0)
goto done;
fuse_session_process_buf(session, &buffer);
}
done:
free(buffer.mem);
if (result > 0)
result = 0;
fuse_session_reset(session);
return result;
}
static char * uriDecode(const char * value, size_t length)
{
static const char hex[] = "0123456789abcdef";
char * result = malloc(length + 1U);
if (!result)
return NULL;
size_t out = 0;
for (size_t i = 0; i < length; ++i)
{
unsigned char ch = (unsigned char)value[i];
if (ch == '%')
{
if (i + 2U >= length)
goto invalid;
const char * high = strchr(hex, (char)(value[i + 1U] | 0x20));
const char * low = strchr(hex, (char)(value[i + 2U] | 0x20));
if (!high || !low)
goto invalid;
ch = (unsigned char)(((high - hex) << 4) | (low - hex));
i += 2U;
}
if (!ch)
goto invalid;
result[out++] = (char)ch;
}
result[out] = 0;
return result;
invalid:
free(result);
return NULL;
}
static bool localNodeStat(struct LocalNode * node, const struct stat * value)
{
if (S_ISREG(value->st_mode))
node->type = KVMFR_CLIPBOARD_FILE_TYPE_REGULAR;
else if (S_ISDIR(value->st_mode))
node->type = KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY;
else
return false;
node->size = value->st_size < 0 ? 0 : (uint64_t)value->st_size;
node->modifiedNs = nsFromTimespec(value->st_mtim);
node->createdNs = 0;
node->device = value->st_dev;
node->inode = value->st_ino;
return true;
}
static bool localStatSupported(const struct stat * value)
{
return S_ISREG(value->st_mode) || S_ISDIR(value->st_mode);
}
static bool localStatEqual(const struct stat * first,
const struct stat * second)
{
return first->st_dev == second->st_dev &&
first->st_ino == second->st_ino &&
((S_ISREG(first->st_mode) && S_ISREG(second->st_mode)) ||
(S_ISDIR(first->st_mode) && S_ISDIR(second->st_mode))) &&
first->st_size == second->st_size &&
first->st_mtim.tv_sec == second->st_mtim.tv_sec &&
first->st_mtim.tv_nsec == second->st_mtim.tv_nsec;
}
static bool localNodeMatches(const struct LocalNode * node,
const struct stat * value)
{
return node->device == value->st_dev && node->inode == value->st_ino &&
((node->type == KVMFR_CLIPBOARD_FILE_TYPE_REGULAR &&
S_ISREG(value->st_mode)) ||
(node->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY &&
S_ISDIR(value->st_mode))) &&
node->size == (value->st_size < 0 ? 0 : (uint64_t)value->st_size) &&
node->modifiedNs == nsFromTimespec(value->st_mtim);
}
static int openAbsoluteLocal(const char * path, char ** name,
struct stat * value)
{
if (!path || path[0] != '/' || !path[1])
return -1;
int current = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (current < 0)
return -1;
const char * component = path + 1;
for (;;)
{
const char * slash = strchr(component, '/');
const size_t length = slash ? (size_t)(slash - component) :
strlen(component);
if (!length || length > NAME_MAX ||
(length == 1U && component[0] == '.') ||
(length == 2U && component[0] == '.' && component[1] == '.'))
goto failed;
char * part = strndup(component, length);
if (!part)
goto failed;
struct stat before;
const bool final = !slash;
int next = -1;
if (fstatat(current, part, &before, AT_SYMLINK_NOFOLLOW) == 0 &&
!S_ISLNK(before.st_mode) &&
(final ? localStatSupported(&before) : S_ISDIR(before.st_mode)))
{
int flags = O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK;
if (!final || S_ISDIR(before.st_mode))
flags |= O_DIRECTORY;
next = openat(current, part, flags);
}
if (next < 0)
{
free(part);
goto failed;
}
struct stat after;
if (fstat(next, &after) < 0 || !localStatEqual(&before, &after))
{
close(next);
free(part);
goto failed;
}
close(current);
current = next;
if (final)
{
*name = part;
*value = after;
return current;
}
free(part);
component = slash + 1;
}
failed:
close(current);
return -1;
}
static int openLocalNode(const struct LocalNode * node,
struct stat * value)
{
if (!node || !node->root || node->root->fd < 0)
return -1;
int current;
if (node->root->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY)
current = openat(node->root->fd, ".",
O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
else
current = fcntl(node->root->fd, F_DUPFD_CLOEXEC, 0);
if (current < 0)
return -1;
const char * component = node->path;
while (*component)
{
const char * slash = strchr(component, '/');
const size_t length = slash ? (size_t)(slash - component) :
strlen(component);
if (!length || length > NAME_MAX ||
(length == 1U && component[0] == '.') ||
(length == 2U && component[0] == '.' && component[1] == '.'))
goto failed;
char * part = strndup(component, length);
if (!part)
goto failed;
int flags = O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK;
if (slash || node->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY)
flags |= O_DIRECTORY;
const int next = openat(current, part, flags);
free(part);
if (next < 0)
goto failed;
close(current);
current = next;
component = slash ? slash + 1 : component + length;
}
if (fstat(current, value) < 0)
goto failed;
if (!localNodeMatches(node, value))
{
errno = ESTALE;
goto failed;
}
return current;
failed:
close(current);
return -1;
}
static void freeLocalDataset(struct LocalDataset * dataset)
{
if (!dataset)
return;
struct LocalNode * node = dataset->nodes;
while (node)
{
struct LocalNode * next = node->next;
if (node->fd >= 0)
close(node->fd);
free(node->path);
free(node->name);
free(node);
node = next;
}
free(dataset);
}
static struct LocalDataset * takeUnusedLocalDatasetNL(
struct LocalDataset * dataset)
{
if (!dataset || dataset->current || dataset->references)
return NULL;
struct LocalDataset ** link = &files.localDatasets;
while (*link && *link != dataset)
link = &(*link)->next;
if (!*link)
return NULL;
*link = dataset->next;
dataset->next = NULL;
return dataset;
}
static char * uriPath(const char * line, size_t length)
{
if (length < strlen(FILE_URI_PREFIX) ||
strncasecmp(line, FILE_URI_PREFIX, strlen(FILE_URI_PREFIX)))
return NULL;
line += strlen(FILE_URI_PREFIX);
length -= strlen(FILE_URI_PREFIX);
if (length >= 10U && !strncasecmp(line, "localhost/", 10U))
{
line += 9U;
length -= 9U;
}
else if (!length || line[0] != '/')
return NULL;
static const char punctuation[] = "-._~!$&'()*+,;=:@/%";
for (size_t i = 0; i < length; ++i)
{
const unsigned char ch = (unsigned char)line[i];
if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') || strchr(punctuation, ch)))
return NULL;
}
char * path = uriDecode(line, length);
if (!path)
return NULL;
size_t pathLength = strlen(path);
while (pathLength > 1U && path[pathLength - 1U] == '/')
path[--pathLength] = 0;
return path;
}
static struct LocalNode * localNodeByIdNL(struct LocalDataset * dataset,
uint64_t node)
{
for (struct LocalNode * item = dataset->nodes; item; item = item->next)
if (item->node == node)
return item;
return NULL;
}
static struct LocalNode * localChildNL(struct LocalDataset * dataset,
uint64_t parent, const char * name)
{
for (struct LocalNode * item = dataset->nodes; item; item = item->next)
if (item->parent == parent && !strcmp(item->name, name))
return item;
return NULL;
}
static struct LocalNode * addLocalNode(struct LocalDataset * dataset,
uint64_t parent, struct LocalNode * root, char * path,
const char * name, const struct stat * value)
{
struct LocalNode * node = calloc(1, sizeof(*node));
if (!node)
{
free(path);
return NULL;
}
node->fd = -1;
node->path = path;
node->name = strdup(name);
if (!node->name || !localNodeStat(node, value))
{
free(node->path);
free(node->name);
free(node);
return NULL;
}
node->dataset = dataset;
node->root = root ? root : node;
node->parent = parent;
node->node = ++dataset->nodeSerial;
node->next = dataset->nodes;
dataset->nodes = node;
return node;
}
bool clipboardFiles_setLocal(const char * mime,
const void * data, size_t size)
{
if (!mime || (!data && size))
return false;
const bool gnome_or_mate =
!strcmp(mime, FILE_GNOME_MIME) ||
!strcmp(mime, FILE_MATE_MIME );
if (strcmp(mime, FILE_URI_MIME) && !gnome_or_mate)
return false;
const char * text = data;
size_t offset = 0;
if (gnome_or_mate)
{
const char * newline = size ? memchr(text, '\n', size) : NULL;
if (!newline)
return false;
const size_t operationLength = (size_t)(newline - text);
if ((operationLength != 4U || memcmp(text, "copy", 4U)) &&
(operationLength != 3U || memcmp(text, "cut", 3U)))
return false;
offset = operationLength + 1U;
}
struct LocalDataset * dataset = calloc(1, sizeof(*dataset));
if (!dataset)
return false;
while (offset < size)
{
const char * line = text + offset;
const char * newline = memchr(line, '\n', size - offset);
size_t length = newline ? (size_t)(newline - line) : size - offset;
if (length && line[length - 1U] == '\r')
--length;
offset += (newline ? (size_t)(newline - line) + 1U : size - offset);
if (!length || line[0] == '#')
continue;
char * path = uriPath(line, length);
if (!path)
goto invalid;
char * name = NULL;
struct stat value;
const int fd = openAbsoluteLocal(path, &name, &value);
free(path);
if (fd < 0)
{
free(name);
goto invalid;
}
char * relative = strdup("");
if (!relative || !validUtf8((const uint8_t *)name, strlen(name)) ||
localChildNL(dataset, KVMFR_CLIPBOARD_FILE_ROOT_NODE, name))
{
close(fd);
free(relative);
free(name);
goto invalid;
}
struct LocalNode * node = addLocalNode(dataset,
KVMFR_CLIPBOARD_FILE_ROOT_NODE, NULL, relative, name, &value);
free(name);
if (!node)
{
close(fd);
goto invalid;
}
node->fd = fd;
}
if (!dataset->nodes)
goto invalid;
dataset->current = true;
LG_LOCK(files.lock);
dataset->dataset = nextDatasetNL();
struct LocalDataset * retired = NULL;
if (files.localCurrent)
{
files.localCurrent->current = false;
retired = takeUnusedLocalDatasetNL(files.localCurrent);
}
dataset->next = files.localDatasets;
files.localDatasets = dataset;
files.localCurrent = dataset;
const uint64_t datasetId = dataset->dataset;
LG_UNLOCK(files.lock);
freeLocalDataset(retired);
clipboard_notifyFiles(datasetId);
return true;
invalid:
freeLocalDataset(dataset);
return false;
}
void clipboardFiles_clearLocal(void)
{
LG_LOCK(files.lock);
struct LocalDataset * retired = NULL;
if (files.localCurrent)
{
files.localCurrent->current = false;
retired = takeUnusedLocalDatasetNL(files.localCurrent);
}
files.localCurrent = NULL;
LG_UNLOCK(files.lock);
freeLocalDataset(retired);
}
static char * uriEncodePath(const char * path)
{
static const char hex[] = "0123456789ABCDEF";
const size_t length = strlen(path);
if (length > (SIZE_MAX - 1U) / 3U)
return NULL;
char * result = malloc(length * 3U + 1U);
if (!result)
return NULL;
size_t out = 0;
for (size_t i = 0; i < length; ++i)
{
const unsigned char ch = (unsigned char)path[i];
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') || ch == '/' || ch == '-' ||
ch == '_' || ch == '.' || ch == '~')
result[out++] = (char)ch;
else
{
result[out++] = '%';
result[out++] = hex[ch >> 4];
result[out++] = hex[ch & 0xfU];
}
}
result[out] = 0;
return result;
}
static bool getRemoteDatasetNL(struct RemoteDataset * dataset,
const char * mime, char ** data, size_t * size)
{
if (!strcmp(mime, FILE_KDE_CUT_MIME))
{
*data = strdup("0");
*size = *data ? 1U : 0U;
return *data != NULL;
}
const bool gnome_or_mate =
!strcmp(mime, FILE_GNOME_MIME) ||
!strcmp(mime, FILE_MATE_MIME );
if (strcmp(mime, FILE_URI_MIME) && !gnome_or_mate)
return false;
char * result = NULL;
size_t used = 0;
size_t capacity = 0;
if (gnome_or_mate)
{
if (!grow((void **)&result, &capacity, 5U))
return false;
memcpy(result, "copy", 4U);
used = 4U;
}
for (struct RemoteNode * node = files.remoteNodes; node; node = node->next)
{
if (node->dataset != dataset || node->parent != dataset->ino)
continue;
const size_t rawLength = strlen(files.mountpoint) + 1U +
strlen(dataset->name) + 1U + strlen(node->name) + 1U;
char * raw = malloc(rawLength);
if (!raw)
goto error;
snprintf(raw, rawLength, "%s/%s/%s",
files.mountpoint, dataset->name, node->name);
char * encoded = uriEncodePath(raw);
free(raw);
if (!encoded)
goto error;
const size_t line = strlen(FILE_URI_PREFIX) + strlen(encoded) + 2U;
if (!grow((void **)&result, &capacity, used + line + 1U))
{
free(encoded);
goto error;
}
if (gnome_or_mate)
used += (size_t)snprintf(result + used, capacity - used,
"\n" FILE_URI_PREFIX "%s", encoded);
else
used += (size_t)snprintf(result + used, capacity - used,
FILE_URI_PREFIX "%s\r\n", encoded);
free(encoded);
}
if (!result)
return false;
*data = result;
*size = used;
return true;
error:
free(result);
return false;
}
bool clipboardFiles_getRemote(const char * mime,
char ** data, size_t * size)
{
if (!mime || !data || !size)
return false;
*data = NULL;
*size = 0;
LG_LOCK(files.lock);
struct RemoteDataset * dataset = files.remoteCurrent;
const bool result = dataset && dataset->ready && !dataset->stale &&
getRemoteDatasetNL(dataset, mime, data, size);
LG_UNLOCK(files.lock);
return result;
}
uint64_t clipboardFiles_remotePresentationAcquire(void)
{
LG_LOCK(files.lock);
struct RemoteDataset * dataset = files.remoteCurrent;
const uint64_t presentation = dataset && dataset->ready &&
dataset->acquired && !dataset->stale ? dataset->acquisition : 0;
if (presentation)
++dataset->presentationPins;
LG_UNLOCK(files.lock);
return presentation;
}
bool clipboardFiles_getRemotePresentation(uint64_t presentation,
const char * mime, char ** data, size_t * size)
{
if (!presentation || !mime || !data || !size)
return false;
*data = NULL;
*size = 0;
LG_LOCK(files.lock);
struct RemoteDataset * dataset = remoteDatasetByAcquisitionNL(presentation);
const bool result = dataset && dataset->presentationPins &&
dataset->ready && !dataset->stale &&
getRemoteDatasetNL(dataset, mime, data, size);
LG_UNLOCK(files.lock);
return result;
}
void clipboardFiles_remotePresentationDelivered(uint64_t presentation)
{
if (!presentation)
return;
LG_LOCK(files.lock);
struct RemoteDataset * dataset =
remoteDatasetByAcquisitionNL(presentation);
const bool delivered = dataset && dataset->presentationPins &&
dataset->ready && dataset->acquired && !dataset->stale;
int wakeFd = -1;
if (delivered)
{
const uint64_t now = microtime();
dataset->deliveryDeadline = now >
UINT64_MAX - FILE_DELIVERY_GRACE_US ? UINT64_MAX :
now + FILE_DELIVERY_GRACE_US;
dataset->deliveryPending = true;
wakeFd = files.fuseStopFd;
}
LG_UNLOCK(files.lock);
if (wakeFd >= 0 && !wakeFuseThread(wakeFd))
DEBUG_WARN("Failed to schedule clipboard file delivery expiry: %s",
strerror(errno));
if (delivered)
reapRemoteDeliveries(true);
}
void clipboardFiles_remotePresentationRelease(uint64_t presentation)
{
if (!presentation)
return;
LG_LOCK(files.lock);
struct RemoteDataset * dataset = remoteDatasetByAcquisitionNL(presentation);
if (dataset && dataset->presentationPins)
--dataset->presentationPins;
const struct RemoteAction action = retireRemoteDatasetNL(dataset);
LG_UNLOCK(files.lock);
performRemoteAction(action);
}
bool clipboardFiles_remoteReady(uint64_t dataset)
{
LG_LOCK(files.lock);
struct RemoteDataset * found = remoteDatasetByIdNL(dataset);
const bool result = found && found->ready && found->current;
LG_UNLOCK(files.lock);
return result;
}
static struct RemoteRequest * takeInitialRequestNL(
struct RemoteDataset * dataset)
{
struct RemoteRequest * request = files.remoteRequests;
while (request && (request->dataset != dataset ||
request->reply != REMOTE_REPLY_INITIAL))
request = request->next;
return request ? remoteRequestTakePointerNL(request) : NULL;
}
static void cancelRetiredInitialRequest(struct RemoteRequest * request)
{
if (!request)
return;
const LG_ClipboardFileRequest descriptor = request->request;
if (request->transportStarted &&
!clipboard_fileCancel(descriptor.dataset, descriptor.request,
LG_CLIPBOARD_FILE_ERROR_CANCELLED))
DEBUG_WARN("Failed to cancel retired clipboard file request");
unregisterRemoteInterrupt(request);
completeRemoteRequest(request);
}
bool clipboardFiles_remoteOffer(uint64_t datasetId)
{
DEBUG_INFO("dataset=%lu",
datasetId);
if (!datasetId)
return false;
struct RemoteDataset * dataset = calloc(1, sizeof(*dataset));
struct RemoteNode * root = calloc(1, sizeof(*root));
if (!dataset || !root)
{
free(dataset);
free(root);
return false;
}
LG_LOCK(files.lock);
if (files.remoteCurrent && files.remoteCurrent->dataset == datasetId &&
!files.remoteCurrent->stale)
{
LG_UNLOCK(files.lock);
free(dataset);
free(root);
return true;
}
struct RemoteDataset * reusable = remoteDatasetByIdNL(datasetId);
if (reusable && reusable->stale)
reusable = NULL;
if (!reusable)
{
dataset->dataset = datasetId;
dataset->acquisition = nextAcquisitionNL();
dataset->ino = nextInodeNL();
dataset->current = true;
dataset->acquiring = true;
snprintf(dataset->name, sizeof(dataset->name),
"%016" PRIx64 "-%016" PRIx64,
datasetId, (uint64_t)dataset->ino);
*root = (struct RemoteNode)
{
.dataset = dataset,
.ino = dataset->ino,
.parent = FUSE_ROOT_ID,
.node = KVMFR_CLIPBOARD_FILE_ROOT_NODE,
.type = KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY,
.name = strdup(dataset->name),
};
if (!root->name)
{
LG_UNLOCK(files.lock);
free(dataset);
free(root);
return false;
}
}
struct RemoteDataset * previous = files.remoteCurrent;
struct RemoteRequest * cancelled = NULL;
struct RemoteAction previousAction = { 0 };
if (previous)
{
previous->current = false;
cancelled = takeInitialRequestNL(previous);
previousAction = retireRemoteDatasetNL(previous);
}
if (reusable)
{
reusable->current = true;
files.remoteCurrent = reusable;
LG_UNLOCK(files.lock);
cancelRetiredInitialRequest(cancelled);
performRemoteAction(previousAction);
reapRemoteDeliveries(true);
free(dataset);
free(root);
return true;
}
dataset->next = files.remoteDatasets;
files.remoteDatasets = dataset;
files.remoteCurrent = dataset;
root->next = files.remoteNodes;
files.remoteNodes = root;
const uint64_t acquisition = dataset->acquisition;
LG_UNLOCK(files.lock);
cancelRetiredInitialRequest(cancelled);
performRemoteAction(previousAction);
reapRemoteDeliveries(true);
if (!clipboard_fileAcquire(datasetId, acquisition))
{
clipboardFiles_remoteCancel(datasetId, acquisition,
LG_CLIPBOARD_FILE_ERROR_DISCONNECTED);
return false;
}
return true;
}
void clipboardFiles_remoteClear(void)
{
LG_LOCK(files.lock);
struct RemoteDataset * previous = files.remoteCurrent;
struct RemoteRequest * cancelled = NULL;
struct RemoteAction action = { 0 };
if (previous)
{
previous->current = false;
cancelled = takeInitialRequestNL(previous);
action = retireRemoteDatasetNL(previous);
}
files.remoteCurrent = NULL;
LG_UNLOCK(files.lock);
cancelRetiredInitialRequest(cancelled);
performRemoteAction(action);
}
void clipboardFiles_providerUnavailable(void)
{
if (!files.initialized)
return;
LG_LOCK(files.lock);
files.remoteCurrent = NULL;
for (struct RemoteDataset * dataset = files.remoteDatasets;
dataset; dataset = dataset->next)
{
dataset->current = false;
dataset->acquiring = false;
dataset->acquired = false;
dataset->stale = true;
dataset->deliveryPending = false;
dataset->deliveryDeadline = 0;
}
struct RemoteRequest * remoteRequests = files.remoteRequests;
files.remoteRequests = NULL;
struct LocalAcquisition * localAcquisitions = files.localAcquisitions;
files.localAcquisitions = NULL;
for (struct LocalAcquisition * acquisition = localAcquisitions;
acquisition; acquisition = acquisition->next)
if (acquisition->dataset->references)
--acquisition->dataset->references;
struct LocalDataset * retired = NULL;
struct LocalDataset ** datasetLink = &files.localDatasets;
while (*datasetLink)
{
struct LocalDataset * dataset = *datasetLink;
if (dataset->current || dataset->references)
{
datasetLink = &dataset->next;
continue;
}
*datasetLink = dataset->next;
dataset->next = retired;
retired = dataset;
}
for (struct LocalJob * job = files.jobsHead; job; job = job->next)
atomic_store_explicit(&job->cancelled, true, memory_order_release);
for (struct LocalJob * job = files.activeJobs; job; job = job->next)
{
atomic_store_explicit(&job->cancelled, true, memory_order_release);
lgSignalEvent(job->ready);
}
LG_UNLOCK(files.lock);
if (files.jobEvent)
lgSignalEvent(files.jobEvent);
while (remoteRequests)
{
struct RemoteRequest * next = remoteRequests->next;
unregisterRemoteInterrupt(remoteRequests);
if (remoteRequests->fuseRequest)
fuse_reply_err(remoteRequests->fuseRequest, EIO);
completeRemoteRequest(remoteRequests);
remoteRequests = next;
}
pruneUnusedRemoteDatasets();
while (localAcquisitions)
{
struct LocalAcquisition * next = localAcquisitions->next;
free(localAcquisitions);
localAcquisitions = next;
}
while (retired)
{
struct LocalDataset * next = retired->next;
freeLocalDataset(retired);
retired = next;
}
}
void clipboardFiles_remoteAcquired(uint64_t datasetId,
uint64_t acquisition, LG_ClipboardFileError error)
{
LG_LOCK(files.lock);
struct RemoteDataset * dataset = remoteDatasetByAcquisitionNL(acquisition);
const bool valid = dataset && dataset->dataset == datasetId &&
dataset->acquiring && !dataset->stale;
if (valid)
{
dataset->acquiring = false;
dataset->acquired = !error;
}
struct RemoteNode * root = valid ? datasetRootNL(dataset) : NULL;
const bool pinned = valid && !error && root &&
pinRemoteOperationNL(dataset, false);
const uint64_t failedDataset = valid && (error || !pinned) ?
failCurrentRemoteDatasetNL(dataset) : 0;
const struct RemoteAction action = valid ?
retireRemoteDatasetNL(dataset) : (struct RemoteAction) { 0 };
LG_UNLOCK(files.lock);
if (valid && error)
DEBUG_WARN("failed: dataset=%lu, acquisition=%lu, error=%u (%s)",
datasetId, acquisition, (unsigned)error, fileErrorName(error));
else if (valid)
DEBUG_INFO("dataset=%lu, acquisition=%lu", datasetId, acquisition);
performRemoteAction(action);
if (failedDataset)
clipboard_fileRemoteFailed(failedDataset);
if (!pinned)
return;
if (!requestRemote(dataset, root, LG_CLIPBOARD_FILE_LIST,
0, 0, REMOTE_REPLY_INITIAL, NULL, 0, 0, NULL))
{
LG_LOCK(files.lock);
const uint64_t unsentDataset =
failCurrentRemoteDatasetNL(dataset);
const struct RemoteAction failed = retireRemoteDatasetNL(dataset);
LG_UNLOCK(files.lock);
performRemoteAction(failed);
if (unsentDataset)
clipboard_fileRemoteFailed(unsentDataset);
}
releaseRemoteOperation(dataset);
}
static bool sameRemoteFileRequest(
const struct RemoteRequest * request,
const LG_ClipboardFileRequest * descriptor)
{
return request->request.dataset == descriptor->dataset &&
request->request.request == descriptor->request &&
request->request.node == descriptor->node &&
request->request.offset == descriptor->offset &&
request->request.length == descriptor->length &&
request->request.operation == descriptor->operation;
}
LG_ClipboardResult clipboardFiles_remoteDataBegin(
const LG_ClipboardFileRequest * descriptor, uint64_t sizeHint)
{
if (!descriptor)
return LG_CLIPBOARD_RESULT_FAILED;
LG_LOCK(files.lock);
struct RemoteRequest * request = files.remoteRequests;
while (request && request->request.request != descriptor->request)
request = request->next;
const bool valid = request && !request->began &&
sameRemoteFileRequest(request, descriptor) &&
(descriptor->operation != LG_CLIPBOARD_FILE_READ ||
sizeHint == descriptor->length);
if (valid)
{
request->began = true;
request->sizeHint = sizeHint;
}
struct RemoteRequest * failed = !valid && request ?
remoteRequestTakeNL(descriptor->request) : NULL;
LG_UNLOCK(files.lock);
failRemoteRequest(failed, EIO);
return valid ? LG_CLIPBOARD_RESULT_ACCEPTED :
LG_CLIPBOARD_RESULT_FAILED;
}
LG_ClipboardResult clipboardFiles_remoteDataChunk(
const LG_ClipboardFileRequest * descriptor, uint64_t responseOffset,
const void * data, size_t size)
{
if (!descriptor)
return LG_CLIPBOARD_RESULT_FAILED;
const bool envelopeValid = data && size && responseOffset <= SIZE_MAX &&
size <= SIZE_MAX - (size_t)responseOffset;
LG_LOCK(files.lock);
struct RemoteRequest * request = files.remoteRequests;
while (request && request->request.request != descriptor->request)
request = request->next;
const size_t end = envelopeValid ? (size_t)responseOffset + size : 0;
const bool valid = envelopeValid && request && request->began &&
sameRemoteFileRequest(request, descriptor) &&
responseOffset == request->dataSize &&
(request->sizeHint == KVMFR_CLIPBOARD_SIZE_UNKNOWN ||
end <= request->sizeHint) &&
(descriptor->operation != LG_CLIPBOARD_FILE_READ ||
end <= descriptor->length) &&
grow((void **)&request->data, &request->dataCapacity, end);
if (valid)
{
memcpy(request->data + request->dataSize, data, size);
request->dataSize = end;
}
struct RemoteRequest * failed = !valid && request ?
remoteRequestTakeNL(descriptor->request) : NULL;
LG_UNLOCK(files.lock);
failRemoteRequest(failed, EIO);
return valid ? LG_CLIPBOARD_RESULT_ACCEPTED :
LG_CLIPBOARD_RESULT_FAILED;
}
LG_ClipboardResult clipboardFiles_remoteDataEnd(
const LG_ClipboardFileRequest * descriptor, uint64_t finalSize)
{
if (!descriptor)
return LG_CLIPBOARD_RESULT_FAILED;
LG_LOCK(files.lock);
struct RemoteRequest * request = remoteRequestTakeNL(descriptor->request);
const bool valid = finalSize <= SIZE_MAX && request && request->began &&
sameRemoteFileRequest(request, descriptor) &&
request->dataSize == (size_t)finalSize &&
(request->sizeHint == KVMFR_CLIPBOARD_SIZE_UNKNOWN ||
request->sizeHint == finalSize) &&
(descriptor->operation != LG_CLIPBOARD_FILE_READ ||
finalSize == descriptor->length);
LG_UNLOCK(files.lock);
if (!valid)
{
failRemoteRequest(request, EIO);
return LG_CLIPBOARD_RESULT_FAILED;
}
finishRemoteRequest(request);
return LG_CLIPBOARD_RESULT_ACCEPTED;
}
void clipboardFiles_remoteCancel(uint64_t dataset,
uint64_t requestId, LG_ClipboardFileError reason)
{
LG_LOCK(files.lock);
struct RemoteRequest * request = remoteRequestTakeNL(requestId);
bool acquisition = false;
uint64_t failedDataset = 0;
struct RemoteAction action = { 0 };
for (struct RemoteDataset * item = files.remoteDatasets;
item; item = item->next)
if (item->dataset == dataset && item->acquisition == requestId)
{
item->acquiring = false;
item->acquired = false;
failedDataset = failCurrentRemoteDatasetNL(item);
item->ready = false;
item->current = false;
item->stale = true;
item->deliveryPending = false;
item->deliveryDeadline = 0;
if (files.remoteCurrent == item)
files.remoteCurrent = NULL;
acquisition = true;
action = retireRemoteDatasetNL(item);
break;
}
LG_UNLOCK(files.lock);
if (acquisition)
DEBUG_WARN("Remote clipboard file acquisition failed: "
"dataset=%lu, acquisition=%lu, error=%u (%s)",
dataset, requestId, (unsigned)reason, fileErrorName(reason));
if (acquisition && !request)
{
performRemoteAction(action);
if (failedDataset)
clipboard_fileRemoteFailed(failedDataset);
return;
}
if (!request || request->request.dataset != dataset)
{
failRemoteRequest(request, EIO);
performRemoteAction(action);
if (failedDataset)
clipboard_fileRemoteFailed(failedDataset);
return;
}
failRemoteRequest(request, errnoFromFileError(reason));
performRemoteAction(action);
if (failedDataset)
clipboard_fileRemoteFailed(failedDataset);
}
void clipboardFiles_localAcquire(uint64_t wireDataset,
uint64_t acquisition)
{
LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_STALE;
struct LocalAcquisition * item = calloc(1, sizeof(*item));
LG_LOCK(files.lock);
struct LocalDataset * dataset = files.localDatasets;
while (dataset && dataset->dataset != wireDataset)
dataset = dataset->next;
if (dataset && item)
{
item->dataset = dataset;
item->wireDataset = wireDataset;
item->acquisition = acquisition;
item->next = files.localAcquisitions;
files.localAcquisitions = item;
++dataset->references;
error = LG_CLIPBOARD_FILE_ERROR_NONE;
}
else if (!item)
error = LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
LG_UNLOCK(files.lock);
if (error)
free(item);
clipboard_fileAcquired(wireDataset, acquisition, error);
}
void clipboardFiles_localRelease(uint64_t wireDataset,
uint64_t acquisition)
{
LG_LOCK(files.lock);
struct LocalAcquisition ** link = &files.localAcquisitions;
while (*link && ((*link)->wireDataset != wireDataset ||
(*link)->acquisition != acquisition))
link = &(*link)->next;
struct LocalAcquisition * item = *link;
struct LocalDataset * retired = NULL;
if (item)
{
*link = item->next;
if (item->dataset->references)
--item->dataset->references;
retired = takeUnusedLocalDatasetNL(item->dataset);
}
LG_UNLOCK(files.lock);
free(item);
freeLocalDataset(retired);
}
static struct LocalDataset * acquiredLocalDatasetNL(uint64_t wireDataset)
{
for (struct LocalDataset * dataset = files.localDatasets;
dataset; dataset = dataset->next)
if (dataset->dataset == wireDataset)
return dataset;
return NULL;
}
static bool sendJobCall(struct LocalJob * job, int operation,
uint64_t offset, const void * data, size_t size, bool end)
{
for (;;)
{
if (atomic_load_explicit(&job->cancelled, memory_order_acquire))
return false;
LG_ClipboardResult result;
switch (operation)
{
case 0:
result = clipboard_fileDataBegin(&job->request, offset);
break;
case 1:
result = clipboard_fileDataChunk(
&job->request, offset, data, size, end);
break;
default:
result = clipboard_fileDataEnd(&job->request, offset);
break;
}
if (result == LG_CLIPBOARD_RESULT_ACCEPTED)
return true;
if (result == LG_CLIPBOARD_RESULT_FAILED)
return false;
if (!lgWaitEvent(job->ready, TIMEOUT_INFINITE))
return false;
}
}
static bool sendBufferJob(struct LocalJob * job,
const uint8_t * data, size_t size)
{
if (!sendJobCall(job, 0, size, NULL, 0, false))
return false;
size_t offset = 0;
while (offset < size)
{
size_t chunk = size - offset;
if (chunk > FILE_STREAM_CHUNK)
chunk = FILE_STREAM_CHUNK;
const bool end = chunk == size - offset;
if (!sendJobCall(job, 1, offset, data + offset, chunk, end))
return false;
offset += chunk;
}
return sendJobCall(job, 2, size, NULL, 0, true);
}
static LG_ClipboardFileError appendLocalEntry(uint8_t ** data, size_t * size,
size_t * capacity, const struct LocalNode * node)
{
const size_t nameLength = strlen(node->name);
if (!nameLength || nameLength > NAME_MAX ||
!validUtf8((const uint8_t *)node->name, nameLength))
return LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED;
const uint64_t bytes64 = KVMFR_CLIPBOARD_FILE_ENTRY_BYTES(nameLength);
if (bytes64 > SIZE_MAX || *size > SIZE_MAX - (size_t)bytes64)
return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
const size_t wanted = *size + (size_t)bytes64;
if (!wanted || !grow((void **)data, capacity, wanted) || !*data)
return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
KVMFRClipboardFileEntry entry =
{
.node = node->node,
.size = node->size,
.createdNs = node->createdNs,
.modifiedNs = node->modifiedNs,
.type = node->type,
.nameLength = (uint32_t)nameLength,
};
memcpy(*data + *size, &entry, sizeof(entry));
memcpy(*data + *size + sizeof(entry), node->name, nameLength);
memset(*data + *size + sizeof(entry) + nameLength, 0,
(size_t)bytes64 - sizeof(entry) - nameLength);
*size += (size_t)bytes64;
return LG_CLIPBOARD_FILE_ERROR_NONE;
}
static bool localJobCancelled(const struct LocalJob * job)
{
return atomic_load_explicit(&job->cancelled, memory_order_acquire);
}
static LG_ClipboardFileError localList(struct LocalJob * job)
{
uint8_t * data = NULL;
size_t size = 0;
size_t capacity = 0;
struct LocalNode ** listed = NULL;
size_t listedCount = 0;
size_t listedCapacity = 0;
LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_IO;
LG_LOCK(files.lock);
struct LocalNode * directory = job->request.node ?
localNodeByIdNL(job->dataset, job->request.node) : NULL;
if (job->request.node && !directory)
{
LG_UNLOCK(files.lock);
return LG_CLIPBOARD_FILE_ERROR_NOT_FOUND;
}
if (directory &&
directory->type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY)
{
LG_UNLOCK(files.lock);
return LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY;
}
if (!job->request.node)
{
struct LocalNode ** roots = NULL;
size_t rootCount = 0;
size_t rootCapacity = 0;
for (struct LocalNode * node = job->dataset->nodes;
node; node = node->next)
if (!node->parent)
{
if (rootCount == SIZE_MAX / sizeof(*roots))
{
LG_UNLOCK(files.lock);
free(roots);
return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
}
if (!grow((void **)&roots, &rootCapacity,
(rootCount + 1U) * sizeof(*roots)))
{
LG_UNLOCK(files.lock);
free(roots);
return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
}
roots[rootCount++] = node;
}
LG_UNLOCK(files.lock);
for (size_t i = 0; i < rootCount; ++i)
{
if (localJobCancelled(job))
{
free(roots);
free(data);
return LG_CLIPBOARD_FILE_ERROR_CANCELLED;
}
struct stat value;
const int fd = openLocalNode(roots[i], &value);
if (fd < 0)
{
error = fileErrorFromErrno(errno);
free(roots);
free(data);
return error;
}
close(fd);
error = appendLocalEntry(&data, &size, &capacity, roots[i]);
if (error)
{
free(roots);
free(data);
return error;
}
}
for (size_t i = 0; i < rootCount; ++i)
{
if (localJobCancelled(job))
{
free(roots);
free(data);
return LG_CLIPBOARD_FILE_ERROR_CANCELLED;
}
struct stat value;
const int fd = openLocalNode(roots[i], &value);
if (fd < 0)
{
error = fileErrorFromErrno(errno);
free(roots);
free(data);
return error;
}
close(fd);
}
free(roots);
const bool result = sendBufferJob(job, data, size);
free(data);
return result ? LG_CLIPBOARD_FILE_ERROR_NONE :
localJobCancelled(job) ? LG_CLIPBOARD_FILE_ERROR_CANCELLED :
LG_CLIPBOARD_FILE_ERROR_DISCONNECTED;
}
LG_UNLOCK(files.lock);
if (localJobCancelled(job))
return LG_CLIPBOARD_FILE_ERROR_CANCELLED;
struct stat directoryBefore;
const int directoryFd = openLocalNode(directory, &directoryBefore);
if (directoryFd < 0)
return fileErrorFromErrno(errno);
DIR * dir = fdopendir(directoryFd);
if (!dir)
{
error = fileErrorFromErrno(errno);
close(directoryFd);
return error;
}
for (;;)
{
if (localJobCancelled(job))
{
error = LG_CLIPBOARD_FILE_ERROR_CANCELLED;
goto failed;
}
errno = 0;
struct dirent * entry = readdir(dir);
if (!entry)
{
if (errno)
{
error = fileErrorFromErrno(errno);
goto failed;
}
break;
}
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
struct stat before;
if (fstatat(directoryFd, entry->d_name, &before,
AT_SYMLINK_NOFOLLOW) < 0)
{
error = fileErrorFromErrno(errno);
goto failed;
}
if (S_ISLNK(before.st_mode))
{
error = LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED;
goto failed;
}
if (!localStatSupported(&before))
{
error = LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED;
goto failed;
}
int flags = O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK;
if (S_ISDIR(before.st_mode))
flags |= O_DIRECTORY;
const int childFd = openat(directoryFd, entry->d_name, flags);
if (childFd < 0)
{
error = fileErrorFromErrno(errno);
goto failed;
}
struct stat value;
if (fstat(childFd, &value) < 0)
{
error = fileErrorFromErrno(errno);
close(childFd);
goto failed;
}
const bool stable = localStatEqual(&before, &value);
close(childFd);
if (!stable)
{
error = LG_CLIPBOARD_FILE_ERROR_STALE;
goto failed;
}
const size_t parentLength = strlen(directory->path);
const size_t nameLength = strlen(entry->d_name);
if (parentLength > SIZE_MAX - nameLength - 2U)
{
error = LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
goto failed;
}
const size_t pathLength = parentLength + (parentLength ? 1U : 0U) +
nameLength + 1U;
char * childPath = malloc(pathLength);
if (!childPath)
{
error = LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
goto failed;
}
if (parentLength)
snprintf(childPath, pathLength, "%s/%s",
directory->path, entry->d_name);
else
memcpy(childPath, entry->d_name, nameLength + 1U);
LG_LOCK(files.lock);
struct LocalNode * node = localChildNL(
job->dataset, directory->node, entry->d_name);
if (!node)
node = addLocalNode(job->dataset, directory->node,
directory->root, childPath, entry->d_name, &value);
else
free(childPath);
if (!node)
error = LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
else if (!localNodeMatches(node, &value))
error = LG_CLIPBOARD_FILE_ERROR_STALE;
else
error = appendLocalEntry(&data, &size, &capacity, node);
LG_UNLOCK(files.lock);
if (error)
goto failed;
if (listedCount == SIZE_MAX / sizeof(*listed) ||
!grow((void **)&listed, &listedCapacity,
(listedCount + 1U) * sizeof(*listed)))
{
error = LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
goto failed;
}
listed[listedCount++] = node;
}
for (size_t i = 0; i < listedCount; ++i)
{
if (localJobCancelled(job))
{
error = LG_CLIPBOARD_FILE_ERROR_CANCELLED;
goto failed;
}
struct stat value;
const int fd = openLocalNode(listed[i], &value);
if (fd < 0)
{
error = fileErrorFromErrno(errno);
goto failed;
}
close(fd);
}
struct stat directoryAfter;
if (localJobCancelled(job))
{
error = LG_CLIPBOARD_FILE_ERROR_CANCELLED;
goto failed;
}
if (fstat(directoryFd, &directoryAfter) < 0)
{
error = fileErrorFromErrno(errno);
goto failed;
}
if (!localNodeMatches(directory, &directoryAfter))
{
error = LG_CLIPBOARD_FILE_ERROR_STALE;
goto failed;
}
if (closedir(dir) < 0)
{
const int closeError = errno;
free(listed);
free(data);
return fileErrorFromErrno(closeError);
}
free(listed);
const bool result = sendBufferJob(job, data, size);
free(data);
return result ? LG_CLIPBOARD_FILE_ERROR_NONE :
localJobCancelled(job) ? LG_CLIPBOARD_FILE_ERROR_CANCELLED :
LG_CLIPBOARD_FILE_ERROR_DISCONNECTED;
failed:
closedir(dir);
free(listed);
free(data);
return error;
}
static LG_ClipboardFileError localRead(struct LocalJob * job)
{
LG_LOCK(files.lock);
struct LocalNode * node = localNodeByIdNL(
job->dataset, job->request.node);
LG_UNLOCK(files.lock);
if (!node)
return LG_CLIPBOARD_FILE_ERROR_NOT_FOUND;
if (node->type != KVMFR_CLIPBOARD_FILE_TYPE_REGULAR)
return LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY;
if (job->request.offset > node->size ||
job->request.length > node->size - job->request.offset)
return LG_CLIPBOARD_FILE_ERROR_INVALID;
if (localJobCancelled(job))
return LG_CLIPBOARD_FILE_ERROR_CANCELLED;
struct stat before;
const int fd = openLocalNode(node, &before);
if (fd < 0)
return fileErrorFromErrno(errno);
uint8_t * buffer = malloc(job->request.length ? job->request.length : 1U);
if (!buffer)
{
close(fd);
return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
}
LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_NONE;
size_t received = 0;
while (received < job->request.length)
{
if (localJobCancelled(job))
{
error = LG_CLIPBOARD_FILE_ERROR_CANCELLED;
goto failed;
}
#ifdef ENABLE_TESTS
const bool forceEof = atomic_exchange_explicit(
&forceLocalEof, false, memory_order_acq_rel);
const ssize_t bytes = forceEof ? 0 :
pread(fd, buffer + received, job->request.length - received,
(off_t)(job->request.offset + received));
#else
const ssize_t bytes = pread(fd, buffer + received,
job->request.length - received,
(off_t)(job->request.offset + received));
#endif
if (bytes < 0 && errno == EINTR)
continue;
if (bytes < 0)
{
error = fileErrorFromErrno(errno);
goto failed;
}
if (!bytes)
{
error = LG_CLIPBOARD_FILE_ERROR_IO;
goto failed;
}
received += (size_t)bytes;
}
struct stat after;
if (localJobCancelled(job))
{
error = LG_CLIPBOARD_FILE_ERROR_CANCELLED;
goto failed;
}
if (fstat(fd, &after) < 0)
{
error = fileErrorFromErrno(errno);
goto failed;
}
if (!localNodeMatches(node, &after) ||
!localStatEqual(&before, &after))
{
error = LG_CLIPBOARD_FILE_ERROR_STALE;
goto failed;
}
if (received != job->request.length)
{
error = LG_CLIPBOARD_FILE_ERROR_IO;
goto failed;
}
close(fd);
const bool result = sendBufferJob(job, buffer, received);
free(buffer);
return result ? LG_CLIPBOARD_FILE_ERROR_NONE :
localJobCancelled(job) ? LG_CLIPBOARD_FILE_ERROR_CANCELLED :
LG_CLIPBOARD_FILE_ERROR_DISCONNECTED;
failed:
close(fd);
free(buffer);
return error;
}
static int localWorker(void * opaque)
{
(void)opaque;
for (;;)
{
LG_LOCK(files.lock);
struct LocalJob * job = files.jobsHead;
if (job)
{
files.jobsHead = job->next;
if (!files.jobsHead)
files.jobsTail = &files.jobsHead;
job->next = files.activeJobs;
files.activeJobs = job;
}
const bool stopping = files.stopping;
if (!job && !stopping)
lgResetEvent(files.jobEvent);
LG_UNLOCK(files.lock);
if (!job)
{
if (stopping)
return 0;
lgWaitEvent(files.jobEvent, TIMEOUT_INFINITE);
continue;
}
LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_CANCELLED;
if (!atomic_load_explicit(&job->cancelled, memory_order_acquire))
error = job->request.operation == LG_CLIPBOARD_FILE_LIST ?
localList(job) : localRead(job);
if (error && error != LG_CLIPBOARD_FILE_ERROR_CANCELLED &&
!atomic_load_explicit(&job->cancelled, memory_order_acquire))
clipboard_fileCancel(job->request.dataset,
job->request.request, error);
LG_LOCK(files.lock);
struct LocalJob ** link = &files.activeJobs;
while (*link && *link != job)
link = &(*link)->next;
if (*link)
*link = job->next;
if (job->dataset->references)
--job->dataset->references;
struct LocalDataset * retired =
takeUnusedLocalDatasetNL(job->dataset);
LG_UNLOCK(files.lock);
freeLocalDataset(retired);
lgFreeEvent(job->ready);
free(job);
}
}
void clipboardFiles_localRequest(
const LG_ClipboardFileRequest * request)
{
if (!request || !request->request ||
(request->operation != LG_CLIPBOARD_FILE_LIST &&
request->operation != LG_CLIPBOARD_FILE_READ) ||
(request->operation == LG_CLIPBOARD_FILE_LIST &&
(request->offset || request->length)) ||
(request->operation == LG_CLIPBOARD_FILE_READ &&
(!request->length || request->length > FILE_REQUEST_MAX ||
request->offset > INT64_MAX ||
request->length > (uint64_t)INT64_MAX - request->offset)))
{
if (request)
clipboard_fileCancel(request->dataset, request->request,
LG_CLIPBOARD_FILE_ERROR_INVALID);
return;
}
struct LocalJob * job = calloc(1, sizeof(*job));
if (!job)
{
clipboard_fileCancel(request->dataset, request->request,
LG_CLIPBOARD_FILE_ERROR_NO_MEMORY);
return;
}
job->ready = lgCreateEvent(true, 0);
if (!job->ready)
{
free(job);
clipboard_fileCancel(request->dataset, request->request,
LG_CLIPBOARD_FILE_ERROR_NO_MEMORY);
return;
}
atomic_init(&job->cancelled, false);
job->request = *request;
LG_LOCK(files.lock);
job->dataset = acquiredLocalDatasetNL(request->dataset);
if (job->dataset && !files.stopping)
{
++job->dataset->references;
*files.jobsTail = job;
files.jobsTail = &job->next;
}
const bool queued = job->dataset && !files.stopping;
LG_UNLOCK(files.lock);
if (queued)
{
lgSignalEvent(files.jobEvent);
return;
}
lgFreeEvent(job->ready);
free(job);
clipboard_fileCancel(request->dataset, request->request,
LG_CLIPBOARD_FILE_ERROR_STALE);
}
void clipboardFiles_localCancel(uint64_t dataset,
uint64_t request, LG_ClipboardFileError reason)
{
(void)reason;
LG_LOCK(files.lock);
struct LocalAcquisition ** acquisition = &files.localAcquisitions;
struct LocalDataset * retired = NULL;
while (*acquisition && ((*acquisition)->wireDataset != dataset ||
(*acquisition)->acquisition != request))
acquisition = &(*acquisition)->next;
if (*acquisition)
{
struct LocalAcquisition * cancelled = *acquisition;
*acquisition = cancelled->next;
if (cancelled->dataset->references)
--cancelled->dataset->references;
retired = takeUnusedLocalDatasetNL(cancelled->dataset);
free(cancelled);
}
for (struct LocalJob * job = files.activeJobs; job; job = job->next)
if (job->request.dataset == dataset && job->request.request == request)
{
atomic_store_explicit(&job->cancelled, true, memory_order_release);
lgSignalEvent(job->ready);
}
for (struct LocalJob * job = files.jobsHead; job; job = job->next)
if (job->request.dataset == dataset && job->request.request == request)
atomic_store_explicit(&job->cancelled, true, memory_order_release);
LG_UNLOCK(files.lock);
freeLocalDataset(retired);
}
void clipboardFiles_localReady(uint64_t request)
{
LG_LOCK(files.lock);
for (struct LocalJob * job = files.activeJobs; job; job = job->next)
if (job->request.request == request)
lgSignalEvent(job->ready);
LG_UNLOCK(files.lock);
}
static void freeState(void)
{
struct RemoteRequest * request = files.remoteRequests;
while (request)
{
struct RemoteRequest * next = request->next;
unregisterRemoteInterrupt(request);
if (request->fuseRequest)
fuse_reply_err(request->fuseRequest, EIO);
freeRemoteRequest(request);
request = next;
}
struct RemoteNode * remoteNode = files.remoteNodes;
while (remoteNode)
{
struct RemoteNode * next = remoteNode->next;
free(remoteNode->name);
free(remoteNode);
remoteNode = next;
}
struct RemoteDataset * remote = files.remoteDatasets;
while (remote)
{
struct RemoteDataset * next = remote->next;
if (remote->acquired)
clipboard_fileRelease(remote->dataset, remote->acquisition);
else if (remote->acquiring)
clipboard_fileCancel(remote->dataset, remote->acquisition,
LG_CLIPBOARD_FILE_ERROR_CANCELLED);
free(remote);
remote = next;
}
struct LocalAcquisition * acquisition = files.localAcquisitions;
while (acquisition)
{
struct LocalAcquisition * next = acquisition->next;
free(acquisition);
acquisition = next;
}
struct LocalDataset * local = files.localDatasets;
while (local)
{
struct LocalDataset * next = local->next;
freeLocalDataset(local);
local = next;
}
}
bool clipboardFiles_init(void)
{
if (filesLockInitialized)
return false;
memset(&files, 0, sizeof(files));
files.fuseStopFd = -1;
LG_LOCK_INIT(files.lock);
filesLockInitialized = true;
uint64_t nonce = 0;
if (!randomBytes(&nonce, sizeof(nonce)))
{
DEBUG_ERROR("Failed to seed clipboard file transfer identifiers: %s",
strerror(errno));
goto failed;
}
nonce &= ~KVMFR_CLIPBOARD_TRANSFER_HELPER;
if (!nonce)
nonce = 1;
files.requestSerial = nonce;
files.datasetSerial = (nonce ^ UINT64_C(0x4c474346494c4553)) &
~KVMFR_CLIPBOARD_TRANSFER_HELPER;
if (!files.datasetSerial)
files.datasetSerial = 1;
files.inodeSerial = FUSE_ROOT_ID;
files.jobsTail = &files.jobsHead;
files.jobEvent = lgCreateEvent(false, 0);
if (!files.jobEvent)
goto failed;
files.fuseStopFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (files.fuseStopFd < 0)
{
DEBUG_ERROR("Failed to create the clipboard FUSE stop event: %s",
strerror(errno));
goto failed;
}
const char * runtime = getenv("XDG_RUNTIME_DIR");
if (!runtime || !*runtime)
{
DEBUG_ERROR("XDG_RUNTIME_DIR is required for clipboard file transfer");
goto failed;
}
const int length = snprintf(files.mountpoint, sizeof(files.mountpoint),
"%s/looking-glass-clipboard-XXXXXX", runtime);
if (length < 0 || (size_t)length >= sizeof(files.mountpoint) ||
!mkdtemp(files.mountpoint))
{
DEBUG_ERROR("Failed to create the clipboard FUSE mountpoint: %s",
strerror(errno));
goto failed;
}
chmod(files.mountpoint, 0700);
char mountOptions[160];
const int optionsLength = snprintf(mountOptions, sizeof(mountOptions),
"ro,nodev,nosuid,noexec,default_permissions,auto_unmount,max_read=%u",
(unsigned)FILE_REQUEST_MAX);
if (optionsLength < 0 || (size_t)optionsLength >= sizeof(mountOptions))
{
DEBUG_ERROR("Failed to format clipboard FUSE mount options");
goto failed;
}
struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
if (fuse_opt_add_arg(&args, "looking-glass-client") < 0 ||
fuse_opt_add_arg(&args, "-o") < 0 ||
fuse_opt_add_arg(&args, mountOptions) < 0)
{
fuse_opt_free_args(&args);
goto failed;
}
files.session = fuse_session_new(&args, &fuseOps,
sizeof(fuseOps), NULL);
fuse_opt_free_args(&args);
if (!files.session || fuse_session_mount(
files.session, files.mountpoint) < 0)
{
DEBUG_ERROR("Failed to mount the clipboard FUSE filesystem");
goto failed;
}
if (!lgCreateThread("clipboard-fuse", fuseThread,
files.session, &files.fuseThread))
goto failed;
unsigned created = 0;
for (; created < FILE_WORKERS; ++created)
if (!lgCreateThread("clipboard-file", localWorker,
NULL, &files.workers[created]))
break;
if (created != FILE_WORKERS)
goto failed;
files.initialized = true;
DEBUG_INFO("Clipboard files mounted at %s", files.mountpoint);
return true;
failed:
clipboardFiles_free();
return false;
}
#ifdef ENABLE_TESTS
bool clipboardFiles_testFuseStopWake(void)
{
const int stopFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (stopFd < 0)
return false;
const bool result = wakeFuseThread(stopFd) &&
waitFuseEvent(-1, stopFd, -1) == FUSE_WAIT_WAKE;
close(stopFd);
return result;
}
bool clipboardFiles_testUnsentRemoteOwnership(void)
{
return finishUnsentRemoteRequest(0);
}
bool clipboardFiles_testInit(uint64_t nonce)
{
if (filesLockInitialized)
return false;
nonce &= ~KVMFR_CLIPBOARD_TRANSFER_HELPER;
if (!nonce)
return false;
memset(&files, 0, sizeof(files));
files.fuseStopFd = -1;
LG_LOCK_INIT(files.lock);
filesLockInitialized = true;
files.requestSerial = nonce;
files.datasetSerial = (nonce ^ UINT64_C(0x4c474346494c4553)) &
~KVMFR_CLIPBOARD_TRANSFER_HELPER;
if (!files.datasetSerial)
files.datasetSerial = 1;
files.inodeSerial = FUSE_ROOT_ID;
files.jobsTail = &files.jobsHead;
files.initialized = true;
atomic_store_explicit(&forceLocalEof, false, memory_order_release);
return true;
}
size_t clipboardFiles_testRemoteDatasetCount(void)
{
if (!filesLockInitialized)
return 0;
size_t count = 0;
LG_LOCK(files.lock);
for (struct RemoteDataset * dataset = files.remoteDatasets;
dataset; dataset = dataset->next)
++count;
LG_UNLOCK(files.lock);
return count;
}
void clipboardFiles_testExpireRemoteDeliveries(void)
{
if (!filesLockInitialized)
return;
LG_LOCK(files.lock);
for (struct RemoteDataset * dataset = files.remoteDatasets;
dataset; dataset = dataset->next)
if (dataset->deliveryPending)
dataset->deliveryDeadline = 0;
LG_UNLOCK(files.lock);
reapRemoteDeliveries(false);
}
bool clipboardFiles_testBeginRemoteLookup(uint64_t presentation)
{
LG_LOCK(files.lock);
struct RemoteDataset * dataset =
remoteDatasetByAcquisitionNL(presentation);
const bool result = dataset && dataset->ready &&
pinRemoteOperationNL(dataset, true);
LG_UNLOCK(files.lock);
return result;
}
void clipboardFiles_testEndRemoteLookup(uint64_t presentation)
{
LG_LOCK(files.lock);
struct RemoteDataset * dataset =
remoteDatasetByAcquisitionNL(presentation);
if (dataset && dataset->operationPins)
--dataset->operationPins;
const struct RemoteAction action = retireRemoteDatasetNL(dataset);
LG_UNLOCK(files.lock);
performRemoteAction(action);
}
bool clipboardFiles_testRemoteRead(uint64_t presentation,
uint64_t nodeId, uint64_t offset, uint32_t length)
{
LG_LOCK(files.lock);
struct RemoteDataset * dataset =
remoteDatasetByAcquisitionNL(presentation);
struct RemoteNode * node = dataset ?
remoteNodeByWireNL(dataset, nodeId) : NULL;
const bool pinned = node && length &&
pinRemoteOperationNL(dataset, false);
LG_UNLOCK(files.lock);
if (!pinned)
return false;
const bool result = requestRemote(dataset, node,
LG_CLIPBOARD_FILE_READ, offset, length, REMOTE_REPLY_READ,
NULL, 0, 0, NULL);
releaseRemoteOperation(dataset);
return result;
}
void clipboardFiles_testForceLocalEof(void)
{
atomic_store_explicit(&forceLocalEof, true, memory_order_release);
}
bool clipboardFiles_testLocalRequest(
const LG_ClipboardFileRequest * request)
{
if (!request ||
(request->operation != LG_CLIPBOARD_FILE_LIST &&
request->operation != LG_CLIPBOARD_FILE_READ))
return false;
struct LocalJob job = { .request = *request };
atomic_init(&job.cancelled, false);
LG_LOCK(files.lock);
job.dataset = acquiredLocalDatasetNL(request->dataset);
if (job.dataset)
++job.dataset->references;
LG_UNLOCK(files.lock);
if (!job.dataset)
return false;
const LG_ClipboardFileError error =
request->operation == LG_CLIPBOARD_FILE_LIST ?
localList(&job) : localRead(&job);
if (error && error != LG_CLIPBOARD_FILE_ERROR_CANCELLED)
clipboard_fileCancel(request->dataset, request->request, error);
LG_LOCK(files.lock);
if (job.dataset->references)
--job.dataset->references;
LG_UNLOCK(files.lock);
return error == LG_CLIPBOARD_FILE_ERROR_NONE;
}
#endif
void clipboardFiles_free(void)
{
if (!filesLockInitialized)
return;
LG_LOCK(files.lock);
files.stopping = true;
for (struct LocalJob * job = files.jobsHead; job; job = job->next)
atomic_store_explicit(&job->cancelled, true, memory_order_release);
for (struct LocalJob * job = files.activeJobs; job; job = job->next)
{
atomic_store_explicit(&job->cancelled, true, memory_order_release);
if (job->ready)
lgSignalEvent(job->ready);
}
LG_UNLOCK(files.lock);
if (files.jobEvent)
lgSignalEvent(files.jobEvent);
for (unsigned i = 0; i < FILE_WORKERS; ++i)
if (files.workers[i])
{
lgSignalEvent(files.jobEvent);
lgJoinThread(files.workers[i], NULL);
files.workers[i] = NULL;
}
if (files.session)
fuse_session_exit(files.session);
if (files.fuseThread)
{
if (!wakeFuseThread(files.fuseStopFd))
DEBUG_WARN("Failed to wake the clipboard FUSE thread: %s",
strerror(errno));
lgJoinThread(files.fuseThread, NULL);
files.fuseThread = NULL;
}
if (files.fuseStopFd >= 0)
{
close(files.fuseStopFd);
files.fuseStopFd = -1;
}
/* Pending requests belong to the stopped but still-live session. */
freeState();
if (files.session)
{
fuse_session_unmount(files.session);
fuse_session_destroy(files.session);
files.session = NULL;
}
if (files.jobEvent)
{
lgFreeEvent(files.jobEvent);
files.jobEvent = NULL;
}
if (files.mountpoint[0] && rmdir(files.mountpoint) < 0 && errno != ENOENT)
DEBUG_WARN("Failed to remove clipboard mountpoint: %s", strerror(errno));
files.mountpoint[0] = 0;
files.initialized = false;
LG_LOCK_FREE(files.lock);
filesLockInitialized = false;
}