From 092e870d2a671d7e16bba2f46c29ccf24a0a2b88 Mon Sep 17 00:00:00 2001 From: ilammy Date: Sun, 9 Apr 2017 02:29:51 +0300 Subject: [PATCH] wClipboard/posix: implement file range retrieval This is another bunch of callbacks which provide the file contents to the clients. We jump through some extra hoops in order to have more pleasant user experience. Simple stuff goes first. The file offset (or position) is split into the low and high parts because this is the format in which the clients receive the request from the server. They can simply copy the values as is into the struct without repackaging them (which we do instead in the end to get a 64-bit off_t). Another thing is that we try to minimize the number of lseek() calls and to keep as few file descriptors open as possible. We cannot open all the files at once as there could be thousands of them and we'll run out of the allowed number of the fds. However, the server can (in theory) request the file ranges randomly so we need to be prepared for that. One way to do that would be to always open the file before reading and close it immediately afterwards. A dead simple solution with an acceptable performance, but... some file systems do not support seeking, such as FTP directories mounted over FUSE. However, they handle sequential reading just fine *and* the server requests the data sequentially most of the time so we can exploit this. Thus open the file only once, during the first range request and keep it open until the server reads all the data. In order to know how much data is left we keep an accurate account of all reads and maintain the file offset ourselves. This also allows us to avoid calling lseek() when the file offset will not be effectively changed. However, if the server requests some weird offset then we have no choice and will attempt seeking. Unfortunately, we cannot tell whether it is a genuine failure or the file system just does not support seeking, so we do not handle the error further. (One workaround would be to reopen the file and keep reading it until we're at the correct offset.) In this way we can support sequential-only file systems if the server requests the contents sequentially (and it does). Also note that we do an fstat() right after opening the file in order to have an accurate value of file size, for this exact file descriptor we will be using. We should have it filled it by now, but just in case... There is one more thing to explain. The cbRequested field specifies the maximum number of bytes the server can handle, not the required number. Usually this is some power-of-two number like 64 KB, based on the size of the files on the clipboard. This is why posix_file_read_perform() does not attempt to fill the whole buffer by repeatedly calling read() if it has read less data than requested. The server can handle underruns just fine (and this spares us from special-casing the EOF condition). --- winpr/include/winpr/clipboard.h | 16 +++ winpr/libwinpr/clipboard/posix.c | 208 +++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+) diff --git a/winpr/include/winpr/clipboard.h b/winpr/include/winpr/clipboard.h index ebede5655..c1c84d71b 100644 --- a/winpr/include/winpr/clipboard.h +++ b/winpr/include/winpr/clipboard.h @@ -34,6 +34,16 @@ struct _wClipboardFileSizeRequest }; typedef struct _wClipboardFileSizeRequest wClipboardFileSizeRequest; +struct _wClipboardFileRangeRequest +{ + UINT32 streamId; + UINT32 listIndex; + UINT32 nPositionLow; + UINT32 nPositionHigh; + UINT32 cbRequested; +}; +typedef struct _wClipboardFileRangeRequest wClipboardFileRangeRequest; + typedef struct _wClipboardDelegate wClipboardDelegate; struct _wClipboardDelegate @@ -46,6 +56,12 @@ struct _wClipboardDelegate UINT64 fileSize); UINT (*ClipboardFileSizeFailure)(wClipboardDelegate*, const wClipboardFileSizeRequest*, UINT errorCode); + + UINT (*ClientRequestFileRange)(wClipboardDelegate*, const wClipboardFileRangeRequest*); + UINT (*ClipboardFileRangeSuccess)(wClipboardDelegate*, const wClipboardFileRangeRequest*, + const BYTE* data, UINT32 size); + UINT (*ClipboardFileRangeFailure)(wClipboardDelegate*, const wClipboardFileRangeRequest*, + UINT errorCode); }; #ifdef __cplusplus diff --git a/winpr/libwinpr/clipboard/posix.c b/winpr/libwinpr/clipboard/posix.c index da934dc46..9f2d98d62 100644 --- a/winpr/libwinpr/clipboard/posix.c +++ b/winpr/libwinpr/clipboard/posix.c @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -49,6 +50,9 @@ struct posix_file char* local_name; WCHAR* remote_name; BOOL is_directory; + + int fd; + off_t offset; off_t size; }; @@ -61,6 +65,9 @@ static struct posix_file* make_posix_file(const char* local_name, const WCHAR* r if (!file) return NULL; + file->fd = -1; + file->offset = 0; + file->local_name = _strdup(local_name); file->remote_name = _wcsdup(remote_name); @@ -94,6 +101,15 @@ static void free_posix_file(void* the_file) if (!file) return; + if (file->fd >= 0) + { + if (close(file->fd) < 0) + { + int err = errno; + WLog_WARN(TAG, "failed to close fd %d: %s", file->fd, strerror(err)); + } + } + free(file->local_name); free(file->remote_name); free(file); @@ -633,6 +649,184 @@ static UINT posix_file_request_size(wClipboardDelegate* delegate, return NO_ERROR; } +static UINT posix_file_read_open(struct posix_file* file) +{ + struct stat statbuf; + + if (file->fd >= 0) + return NO_ERROR; + + file->fd = open(file->local_name, O_RDONLY); + if (file->fd < 0) + { + int err = errno; + WLog_ERR(TAG, "failed to open file %s: %s", file->local_name, strerror(err)); + return ERROR_FILE_NOT_FOUND; + } + + if (fstat(file->fd, &statbuf) < 0) + { + int err = errno; + WLog_ERR(TAG, "failed to stat file: %s", strerror(err)); + return ERROR_FILE_INVALID; + } + + file->offset = 0; + file->size = statbuf.st_size; + +#ifdef WITH_DEBUG_WCLIPBOARD + WLog_DBG(TAG, "open file %d -> %s", file->fd, file->local_name); + WLog_DBG(TAG, "file %d size: %"PRIu64" bytes", file->fd, file->size); +#endif + + return NO_ERROR; +} + +static UINT posix_file_read_seek(struct posix_file* file, UINT64 offset) +{ + /* + * We should avoid seeking when possible as some filesystems (e.g., + * an FTP server mapped via FUSE) may not support seeking. We keep + * an accurate account of the current file offset and do not call + * lseek() if the client requests file content sequentially. + */ + if (file->offset == offset) + return NO_ERROR; + +#ifdef WITH_DEBUG_WCLIPBOARD + WLog_DBG(TAG, "file %d force seeking to %"PRIu64", current %"PRIu64, file->fd, + offset, file->offset); +#endif + + if (lseek(file->fd, offset, SEEK_SET) < 0) + { + int err = errno; + WLog_ERR(TAG, "failed to seek file: %s", strerror(err)); + return ERROR_SEEK; + } + + return NO_ERROR; +} + +static UINT posix_file_read_perform(struct posix_file* file, UINT32 size, + BYTE** actual_data, UINT32* actual_size) +{ + BYTE* buffer = NULL; + ssize_t amount = 0; + +#ifdef WITH_DEBUG_WCLIPBOARD + WLog_DBG(TAG, "file %d request read %"PRIu32" bytes", file->fd, size); +#endif + + buffer = malloc(size); + if (!buffer) + { + WLog_ERR(TAG, "failed to allocate %"PRIu32" buffer bytes", size); + return ERROR_NOT_ENOUGH_MEMORY; + } + + amount = read(file->fd, buffer, size); + if (amount < 0) + { + int err = errno; + WLog_ERR(TAG, "failed to read file: %s", strerror(err)); + goto error; + } + + *actual_data = buffer; + *actual_size = amount; + file->offset += amount; + +#ifdef WITH_DEBUG_WCLIPBOARD + WLog_DBG(TAG, "file %d actual read %"PRIu32" bytes (offset %"PRIu64")", file->fd, + amount, file->offset); +#endif + + return NO_ERROR; + +error: + free(buffer); + + return ERROR_READ_FAULT; +} + +static UINT posix_file_read_close(struct posix_file* file) +{ + if (file->fd < 0) + return NO_ERROR; + + if (file->offset == file->size) + { +#ifdef WITH_DEBUG_WCLIPBOARD + WLog_DBG(TAG, "close file %d", file->fd); +#endif + if (close(file->fd) < 0) + { + int err = errno; + WLog_WARN(TAG, "failed to close fd %d: %s", file->fd, strerror(err)); + } + file->fd = -1; + } + + return NO_ERROR; +} + +static UINT posix_file_get_range(struct posix_file* file, UINT64 offset, UINT32 size, + BYTE** actual_data, UINT32* actual_size) +{ + UINT error = NO_ERROR; + + error = posix_file_read_open(file); + if (error) + goto out; + + error = posix_file_read_seek(file, offset); + if (error) + goto out; + + error = posix_file_read_perform(file, size, actual_data, actual_size); + if (error) + goto out; + + error = posix_file_read_close(file); + if (error) + goto out; +out: + return error; +} + +static UINT posix_file_request_range(wClipboardDelegate* delegate, + const wClipboardFileRangeRequest* request) +{ + UINT error = 0; + BYTE* data = NULL; + UINT32 size = 0; + UINT64 offset = 0; + struct posix_file* file = NULL; + + if (!delegate || !delegate->clipboard || !request) + return ERROR_BAD_ARGUMENTS; + + file = ArrayList_GetItem(delegate->clipboard->localFiles, request->listIndex); + if (!file) + return ERROR_INDEX_ABSENT; + + offset = (((UINT64) request->nPositionHigh) << 32) | ((UINT64) request->nPositionLow); + error = posix_file_get_range(file, offset, request->cbRequested, &data, &size); + + if (error) + error = delegate->ClipboardFileRangeFailure(delegate, request, error); + else + error = delegate->ClipboardFileRangeSuccess(delegate, request, data, size); + + if (error) + WLog_WARN(TAG, "failed to report file range result: 0x%08X", error); + + free(data); + + return NO_ERROR; +} + static UINT dummy_file_size_success(wClipboardDelegate* delegate, const wClipboardFileSizeRequest* request, UINT64 fileSize) { return ERROR_NOT_SUPPORTED; @@ -643,11 +837,25 @@ static UINT dummy_file_size_failure(wClipboardDelegate* delegate, const wClipboa return ERROR_NOT_SUPPORTED; } +static UINT dummy_file_range_success(wClipboardDelegate* delegate, const wClipboardFileRangeRequest* request, const BYTE* data, UINT32 size) +{ + return ERROR_NOT_SUPPORTED; +} + +static UINT dummy_file_range_failure(wClipboardDelegate* delegate, const wClipboardFileRangeRequest* request, UINT errorCode) +{ + return ERROR_NOT_SUPPORTED; +} + static void setup_delegate(wClipboardDelegate* delegate) { delegate->ClientRequestFileSize = posix_file_request_size; delegate->ClipboardFileSizeSuccess = dummy_file_size_success; delegate->ClipboardFileSizeFailure = dummy_file_size_failure; + + delegate->ClientRequestFileRange = posix_file_request_range; + delegate->ClipboardFileRangeSuccess = dummy_file_range_success; + delegate->ClipboardFileRangeFailure = dummy_file_range_failure; } BOOL ClipboardInitPosixFileSubsystem(wClipboard* clipboard)