From 9ce121f97e6b01f66bc92b42947f89b4a37f9a3d Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Fri, 14 Aug 2026 14:27:46 +1000 Subject: [PATCH] [idd] helper: add clipboard file primitives --- idd/LGIddHelper/CClipboardFiles.cpp | 1968 +++++++++++++++++++ idd/LGIddHelper/CClipboardFiles.h | 115 ++ idd/LGIddHelper/LGIddHelper.vcxproj | 5 + idd/LGIddHelper/LGIddHelper.vcxproj.filters | 6 + 4 files changed, 2094 insertions(+) create mode 100644 idd/LGIddHelper/CClipboardFiles.cpp create mode 100644 idd/LGIddHelper/CClipboardFiles.h diff --git a/idd/LGIddHelper/CClipboardFiles.cpp b/idd/LGIddHelper/CClipboardFiles.cpp new file mode 100644 index 00000000..95b32513 --- /dev/null +++ b/idd/LGIddHelper/CClipboardFiles.cpp @@ -0,0 +1,1968 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "CClipboardFiles.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + static constexpr uint64_t WINDOWS_EPOCH_TICKS = + UINT64_C(116444736000000000); + static constexpr size_t COPY_BUFFER_BYTES = + static_cast(64U) * 1024U; + + class CThreadImpersonation final + { + private: + HANDLE m_previousToken = nullptr; + bool m_active = false; + + void ClosePreviousToken() + { + if (m_previousToken) + { + CloseHandle(m_previousToken); + m_previousToken = nullptr; + } + } + + bool RevertInternal() + { + if (!m_active) + return true; + const bool restored = m_previousToken ? + SetThreadToken(nullptr, m_previousToken) != FALSE : + RevertToSelf() != FALSE; + if (restored) + { + m_active = false; + ClosePreviousToken(); + return true; + } + + const DWORD error = GetLastError(); + if (!m_previousToken && SetThreadToken(nullptr, nullptr)) + { + m_active = false; + SetLastError(error); + return false; + } + + RevertToSelf(); + TerminateProcess(GetCurrentProcess(), error ? error : + ERROR_ACCESS_DENIED); + std::terminate(); + } + + public: + explicit CThreadImpersonation(HANDLE token) + { + if (!token) + return; + + if (!OpenThreadToken(GetCurrentThread(), + TOKEN_QUERY | TOKEN_IMPERSONATE, TRUE, &m_previousToken)) + { + const DWORD error = GetLastError(); + if (error != ERROR_NO_TOKEN) + return; + SetLastError(ERROR_SUCCESS); + } + + if (SetThreadToken(nullptr, token)) + m_active = true; + else + { + const DWORD error = GetLastError(); + ClosePreviousToken(); + SetLastError(error); + } + } + + ~CThreadImpersonation() + { + const DWORD error = GetLastError(); + bool restored = true; + if (m_active) + restored = RevertInternal(); + ClosePreviousToken(); + if (restored) + SetLastError(error); + } + + bool Active() const + { + return m_active; + } + + bool Finish() + { + const DWORD error = GetLastError(); + if (!RevertInternal()) + return false; + SetLastError(error); + return true; + } + }; + + KVMFRClipboardFileError TokenError(DWORD error) + { + switch (error) + { + case ERROR_NOT_ENOUGH_MEMORY: + case ERROR_OUTOFMEMORY: + return KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY; + default: + return KVMFR_CLIPBOARD_FILE_ERROR_ACCESS; + } + } + + bool CaptureUserToken(HANDLE& token, + KVMFRClipboardFileError& error) + { + token = nullptr; + DWORD sessionId = 0; + if (!ProcessIdToSessionId(GetCurrentProcessId(), &sessionId)) + { + error = TokenError(GetLastError()); + return false; + } + + HANDLE processToken = nullptr; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, + &processToken)) + { + error = TokenError(GetLastError()); + return false; + } + + HANDLE brokerToken = nullptr; + const bool brokerDuplicated = DuplicateTokenEx(processToken, + TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES | TOKEN_IMPERSONATE, nullptr, + SecurityImpersonation, TokenImpersonation, &brokerToken) != FALSE; + const DWORD brokerError = brokerDuplicated ? ERROR_SUCCESS : + GetLastError(); + CloseHandle(processToken); + if (!brokerDuplicated) + { + SetLastError(brokerError); + error = TokenError(brokerError); + return false; + } + + LUID privilege = {}; + if (!LookupPrivilegeValueW(nullptr, SE_TCB_NAME, &privilege)) + { + const DWORD winError = GetLastError(); + CloseHandle(brokerToken); + SetLastError(winError); + error = TokenError(winError); + return false; + } + + TOKEN_PRIVILEGES privileges = {}; + privileges.PrivilegeCount = 1; + privileges.Privileges[0].Luid = privilege; + privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + SetLastError(ERROR_SUCCESS); + if (!AdjustTokenPrivileges(brokerToken, FALSE, &privileges, 0, nullptr, + nullptr) || GetLastError() != ERROR_SUCCESS) + { + const DWORD winError = GetLastError(); + CloseHandle(brokerToken); + SetLastError(winError ? winError : ERROR_ACCESS_DENIED); + error = TokenError(winError ? winError : ERROR_ACCESS_DENIED); + return false; + } + + CThreadImpersonation brokerImpersonation(brokerToken); + if (!brokerImpersonation.Active()) + { + const DWORD winError = GetLastError(); + CloseHandle(brokerToken); + SetLastError(winError); + error = TokenError(winError); + return false; + } + + HANDLE sourceToken = nullptr; + const bool queried = WTSQueryUserToken(sessionId, &sourceToken) != FALSE; + const DWORD queryError = queried ? ERROR_SUCCESS : GetLastError(); + if (!brokerImpersonation.Finish()) + { + const DWORD winError = GetLastError(); + if (sourceToken) + CloseHandle(sourceToken); + CloseHandle(brokerToken); + SetLastError(winError); + error = TokenError(winError); + return false; + } + CloseHandle(brokerToken); + if (!queried || !sourceToken) + { + if (sourceToken) + CloseHandle(sourceToken); + SetLastError(queryError ? queryError : ERROR_ACCESS_DENIED); + error = TokenError(queryError ? queryError : ERROR_ACCESS_DENIED); + return false; + } + + const bool duplicated = DuplicateTokenEx(sourceToken, + TOKEN_QUERY | TOKEN_IMPERSONATE, nullptr, SecurityImpersonation, + TokenImpersonation, &token) != FALSE; + const DWORD duplicateError = duplicated ? ERROR_SUCCESS : GetLastError(); + CloseHandle(sourceToken); + if (!duplicated) + { + SetLastError(duplicateError); + error = TokenError(duplicateError); + return false; + } + return true; + } + + uint64_t FileTimeToUnixNs(const FILETIME& time) + { + ULARGE_INTEGER value = {}; + value.LowPart = time.dwLowDateTime; + value.HighPart = time.dwHighDateTime; + if (value.QuadPart <= WINDOWS_EPOCH_TICKS) + return 0; + const uint64_t ticks = value.QuadPart - WINDOWS_EPOCH_TICKS; + return ticks <= (std::numeric_limits::max)() / 100U ? + ticks * 100U : (std::numeric_limits::max)(); + } + + FILETIME UnixNsToFileTime(uint64_t ns) + { + ULARGE_INTEGER value = {}; + const uint64_t ticks = ns / 100U; + value.QuadPart = ticks <= + (std::numeric_limits::max)() - WINDOWS_EPOCH_TICKS ? + ticks + WINDOWS_EPOCH_TICKS : + (std::numeric_limits::max)(); + FILETIME result = {}; + result.dwLowDateTime = value.LowPart; + result.dwHighDateTime = value.HighPart; + return result; + } + + KVMFRClipboardFileError FileError(DWORD error) + { + switch (error) + { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_INVALID_HANDLE: + return KVMFR_CLIPBOARD_FILE_ERROR_NOT_FOUND; + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: + case ERROR_LOCK_VIOLATION: + return KVMFR_CLIPBOARD_FILE_ERROR_ACCESS; + case ERROR_DIRECTORY: + return KVMFR_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY; + case ERROR_NOT_ENOUGH_MEMORY: + case ERROR_OUTOFMEMORY: + return KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY; + case ERROR_DISK_FULL: + case ERROR_HANDLE_DISK_FULL: + return KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE; + case ERROR_DEVICE_NOT_CONNECTED: + case ERROR_BROKEN_PIPE: + return KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED; + case ERROR_OPERATION_ABORTED: + case ERROR_CANCELLED: + return KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED; + case ERROR_NOT_SUPPORTED: + return KVMFR_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED; + case ERROR_INVALID_DATA: + case ERROR_INVALID_NAME: + case ERROR_INVALID_PARAMETER: + return KVMFR_CLIPBOARD_FILE_ERROR_INVALID; + default: + return KVMFR_CLIPBOARD_FILE_ERROR_IO; + } + } + + bool SameIdentity(const CLocalClipboardFiles::Node& node, + const BY_HANDLE_FILE_INFORMATION& information) + { + const bool directory = + (information.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; + const uint64_t size = directory ? 0 : + (static_cast(information.nFileSizeHigh) << 32) | + information.nFileSizeLow; + return !(information.dwFileAttributes & FILE_ATTRIBUTE_DEVICE) && + directory == + (node.type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY) && + size == node.size && + FileTimeToUnixNs(information.ftCreationTime) == node.createdNs && + FileTimeToUnixNs(information.ftLastWriteTime) == node.modifiedNs && + information.dwVolumeSerialNumber == node.volumeSerial && + information.nFileIndexHigh == node.fileIndexHigh && + information.nFileIndexLow == node.fileIndexLow; + } + + std::wstring BaseName(std::wstring path) + { + while (path.size() > 1 && + (path.back() == L'\\' || path.back() == L'/')) + { + if (path.size() == 3 && path[1] == L':') + break; + path.pop_back(); + } + size_t end = path.size(); + while (end && (path[end - 1] == L'\\' || path[end - 1] == L'/')) + --end; + const size_t separator = end ? path.find_last_of(L"\\/", end - 1) : + std::wstring::npos; + std::wstring name = path.substr( + separator == std::wstring::npos ? 0 : separator + 1, + end - (separator == std::wstring::npos ? 0 : separator + 1)); + if (name.size() == 2 && name[1] == L':') + name.resize(1); + return name; + } + + std::wstring JoinPath(const std::wstring& parent, + const std::wstring& child) + { + std::wstring result = parent; + if (!result.empty() && result.back() != L'\\' && result.back() != L'/') + result.push_back(L'\\'); + result += child; + return result; + } + + bool HasDotComponent(const std::wstring& path, size_t offset) + { + while (offset < path.size()) + { + const size_t end = path.find(L'\\', offset); + const size_t length = + (end == std::wstring::npos ? path.size() : end) - offset; + if ((length == 1 && path[offset] == L'.') || + (length == 2 && path[offset] == L'.' && + path[offset + 1] == L'.')) + return true; + if (end == std::wstring::npos) + break; + offset = end + 1U; + } + return false; + } + + bool SourcePath(const std::wstring& input, std::wstring& output) + { + if (input.empty()) + { + SetLastError(ERROR_INVALID_NAME); + return false; + } + + try + { + std::wstring path = input; + for (wchar_t& value : path) + if (value == L'/') + value = L'\\'; + + const bool drive = path.size() >= 3 && + ((path[0] >= L'A' && path[0] <= L'Z') || + (path[0] >= L'a' && path[0] <= L'z')) && + path[1] == L':' && path[2] == L'\\'; + const bool unc = path.size() >= 3 && path[0] == L'\\' && + path[1] == L'\\' && path[2] != L'?' && path[2] != L'.'; + const bool extendedDrive = path.size() >= 7 && + path.compare(0, 4, L"\\\\?\\") == 0 && + ((path[4] >= L'A' && path[4] <= L'Z') || + (path[4] >= L'a' && path[4] <= L'z')) && + path[5] == L':' && path[6] == L'\\'; + const bool extendedUNC = path.size() >= 8 && + path.compare(0, 4, L"\\\\?\\") == 0 && + (path[4] == L'U' || path[4] == L'u') && + (path[5] == L'N' || path[5] == L'n') && + (path[6] == L'C' || path[6] == L'c') && path[7] == L'\\'; + + const size_t componentOffset = drive ? 3U : unc ? 2U : + extendedDrive ? 7U : extendedUNC ? 8U : 0U; + if (!componentOffset || HasDotComponent(path, componentOffset)) + { + SetLastError(ERROR_INVALID_NAME); + return false; + } + + if (extendedDrive) + output = std::move(path); + else if (extendedUNC) + { + path[4] = L'U'; + path[5] = L'N'; + path[6] = L'C'; + output = std::move(path); + } + else if (drive) + output = L"\\\\?\\" + path; + else if (unc) + output = L"\\\\?\\UNC\\" + path.substr(2); + else + { + SetLastError(ERROR_INVALID_NAME); + return false; + } + } + catch (const std::bad_alloc&) + { + SetLastError(ERROR_OUTOFMEMORY); + return false; + } + return true; + } + + bool ToUTF8(const std::wstring& text, std::vector& output) + { + if (text.empty() || text.size() > + static_cast((std::numeric_limits::max)())) + { + SetLastError(ERROR_INVALID_DATA); + return false; + } + const int count = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, + text.data(), static_cast(text.size()), nullptr, 0, nullptr, nullptr); + if (count <= 0) + return false; + try + { + const size_t offset = output.size(); + output.resize(offset + static_cast(count)); + if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, + text.data(), static_cast(text.size()), + reinterpret_cast(output.data() + offset), count, + nullptr, nullptr) != count) + { + output.resize(offset); + return false; + } + } + catch (const std::bad_alloc&) + { + SetLastError(ERROR_OUTOFMEMORY); + return false; + } + return true; + } + + bool FromUTF8(const uint8_t * data, size_t length, std::wstring& output) + { + if (!data || !length || length > + static_cast((std::numeric_limits::max)())) + return false; + const int count = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + reinterpret_cast(data), static_cast(length), + nullptr, 0); + if (count <= 0) + return false; + try + { + output.resize(static_cast(count)); + } + catch (const std::bad_alloc&) + { + return false; + } + return MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + reinterpret_cast(data), static_cast(length), + output.data(), count) == count; + } + + bool ValidComponent(const std::wstring& name) + { + if (name.empty() || name.size() > 255 || name == L"." || name == L".." || + name.back() == L'.' || name.back() == L' ') + return false; + for (wchar_t value : name) + if (value < 32 || value == L'\\' || value == L'/' || value == L'<' || + value == L'>' || value == L':' || value == L'"' || value == L'|' || + value == L'?' || value == L'*') + return false; + + std::wstring base = name.substr(0, name.find(L'.')); + for (wchar_t& value : base) + if (value >= L'a' && value <= L'z') + value = static_cast(value - (L'a' - L'A')); + if (base == L"CON" || base == L"PRN" || base == L"AUX" || + base == L"NUL") + return false; + if (base.size() == 4 && + (base.compare(0, 3, L"COM") == 0 || + base.compare(0, 3, L"LPT") == 0)) + { + const wchar_t suffix = base[3]; + if ((suffix >= L'1' && suffix <= L'9') || suffix == L'\u00b9' || + suffix == L'\u00b2' || suffix == L'\u00b3') + return false; + } + return true; + } + + int CompareComponent(const std::wstring& left, + const std::wstring& right) + { + const int result = CompareStringOrdinal(left.c_str(), + static_cast(left.size()), right.c_str(), + static_cast(right.size()), TRUE); + return result ? result : + (left < right ? CSTR_LESS_THAN : + (left == right ? CSTR_EQUAL : CSTR_GREATER_THAN)); + } + + struct ComponentLess + { + bool operator()(const std::wstring& left, + const std::wstring& right) const + { + return CompareComponent(left, right) == CSTR_LESS_THAN; + } + }; + + bool SameComponent(const std::wstring& left, const std::wstring& right) + { + return CompareComponent(left, right) == CSTR_EQUAL; + } + + class CClipboardFileDatasetLease final + { + public: + const uint64_t dataset; + const uint64_t acquisition; + const std::shared_ptr provider; + + CClipboardFileDatasetLease(uint64_t dataset, uint64_t acquisition, + std::shared_ptr provider) : + dataset(dataset), acquisition(acquisition), provider(std::move(provider)) + { + } + + ~CClipboardFileDatasetLease() + { + provider->ReleaseClipboardFileDataset(dataset, acquisition); + } + }; + + class CClipboardFileStream final : public IStream + { + private: + std::atomic m_refs { 1 }; + ClipboardRemoteFileEntry m_entry; + std::shared_ptr m_lease; + std::mutex m_lock; + uint64_t m_offset = 0; + + public: + CClipboardFileStream( + const ClipboardRemoteFileEntry& entry, + std::shared_ptr lease, + uint64_t offset = 0) : + m_entry(entry), m_lease(std::move(lease)), m_offset(offset) + { + } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void ** object) + override + { + if (!object) + return E_POINTER; + *object = nullptr; + if (iid == IID_IUnknown || iid == IID_ISequentialStream || + iid == IID_IStream) + { + *object = static_cast(this); + AddRef(); + return S_OK; + } + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE AddRef() override + { + return m_refs.fetch_add(1, std::memory_order_relaxed) + 1; + } + + ULONG STDMETHODCALLTYPE Release() override + { + const ULONG refs = m_refs.fetch_sub(1, std::memory_order_acq_rel) - 1; + if (!refs) + delete this; + return refs; + } + + HRESULT STDMETHODCALLTYPE Read(void * data, ULONG length, + ULONG * read) override + { + if (read) + *read = 0; + if (!data && length) + return STG_E_INVALIDPOINTER; + std::lock_guard lock(m_lock); + if (!length) + return S_OK; + if (m_offset >= m_entry.size) + return S_FALSE; + const ULONG requested = static_cast((std::min)( + length, m_entry.size - m_offset)); + ULONG actual = 0; + const HRESULT result = m_lease->provider->ReadClipboardFile( + m_lease->dataset, m_lease->acquisition, + m_entry.node, m_offset, data, requested, actual); + if (FAILED(result)) + return result; + if (actual != requested) + return STG_E_READFAULT; + m_offset += actual; + if (read) + *read = actual; + return requested == length ? S_OK : S_FALSE; + } + + HRESULT STDMETHODCALLTYPE Write(const void *, ULONG, ULONG *) override + { + return STG_E_ACCESSDENIED; + } + + HRESULT STDMETHODCALLTYPE Seek(LARGE_INTEGER move, DWORD origin, + ULARGE_INTEGER * position) override + { + std::lock_guard lock(m_lock); + uint64_t base = 0; + switch (origin) + { + case STREAM_SEEK_SET: + base = 0; + break; + case STREAM_SEEK_CUR: + base = m_offset; + break; + case STREAM_SEEK_END: + base = m_entry.size; + break; + default: + return STG_E_INVALIDFUNCTION; + } + + uint64_t next = 0; + if (move.QuadPart < 0) + { + const uint64_t amount = static_cast(-(move.QuadPart + 1)) + + 1U; + if (amount > base) + return STG_E_INVALIDFUNCTION; + next = base - amount; + } + else + { + const uint64_t amount = static_cast(move.QuadPart); + if (amount > (std::numeric_limits::max)() - base) + return STG_E_INVALIDFUNCTION; + next = base + amount; + } + m_offset = next; + if (position) + position->QuadPart = next; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE SetSize(ULARGE_INTEGER) override + { + return STG_E_ACCESSDENIED; + } + + HRESULT STDMETHODCALLTYPE CopyTo(IStream * stream, ULARGE_INTEGER count, + ULARGE_INTEGER * readTotal, ULARGE_INTEGER * writtenTotal) override + { + if (!stream) + return STG_E_INVALIDPOINTER; + if (readTotal) + readTotal->QuadPart = 0; + if (writtenTotal) + writtenTotal->QuadPart = 0; + std::vector buffer; + try + { + buffer.resize(COPY_BUFFER_BYTES); + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + catch (const std::length_error&) + { + return E_OUTOFMEMORY; + } + + uint64_t remaining = count.QuadPart; + while (remaining) + { + const ULONG wanted = static_cast((std::min)( + remaining, buffer.size())); + ULONG got = 0; + HRESULT result = Read(buffer.data(), wanted, &got); + if (FAILED(result)) + return result; + if (!got) + return S_FALSE; + ULONG written = 0; + result = stream->Write(buffer.data(), got, &written); + if (readTotal) + readTotal->QuadPart += got; + if (writtenTotal) + writtenTotal->QuadPart += written; + if (FAILED(result) || written != got) + return FAILED(result) ? result : STG_E_WRITEFAULT; + remaining -= got; + } + return S_OK; + } + + HRESULT STDMETHODCALLTYPE Commit(DWORD) override { return S_OK; } + HRESULT STDMETHODCALLTYPE Revert() override { return STG_E_REVERTED; } + HRESULT STDMETHODCALLTYPE LockRegion(ULARGE_INTEGER, ULARGE_INTEGER, + DWORD) override { return STG_E_INVALIDFUNCTION; } + HRESULT STDMETHODCALLTYPE UnlockRegion(ULARGE_INTEGER, ULARGE_INTEGER, + DWORD) override { return STG_E_INVALIDFUNCTION; } + + HRESULT STDMETHODCALLTYPE Stat(STATSTG * stat, DWORD flags) override + { + if (!stat) + return STG_E_INVALIDPOINTER; + *stat = {}; + stat->type = STGTY_STREAM; + stat->cbSize.QuadPart = m_entry.size; + stat->grfMode = STGM_READ; + stat->ctime = UnixNsToFileTime(m_entry.createdNs); + stat->mtime = UnixNsToFileTime(m_entry.modifiedNs); + if (!(flags & STATFLAG_NONAME)) + { + const size_t bytes = (m_entry.name.size() + 1U) * sizeof(wchar_t); + stat->pwcsName = static_cast(CoTaskMemAlloc(bytes)); + if (!stat->pwcsName) + return E_OUTOFMEMORY; + memcpy(stat->pwcsName, m_entry.name.c_str(), bytes); + } + return S_OK; + } + + HRESULT STDMETHODCALLTYPE Clone(IStream ** stream) override + { + if (!stream) + return E_POINTER; + *stream = nullptr; + std::lock_guard lock(m_lock); + try + { + CClipboardFileStream * clone = new (std::nothrow) + CClipboardFileStream(m_entry, m_lease, m_offset); + if (!clone) + return E_OUTOFMEMORY; + *stream = clone; + return S_OK; + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + catch (const std::length_error&) + { + return E_OUTOFMEMORY; + } + } + }; + + class CFormatEnumerator final : public IEnumFORMATETC + { + private: + std::atomic m_refs { 1 }; + std::vector m_formats; + size_t m_index = 0; + + public: + CFormatEnumerator(std::vector formats, size_t index = 0) : + m_formats(std::move(formats)), m_index(index) + { + } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void ** object) + override + { + if (!object) + return E_POINTER; + *object = nullptr; + if (iid == IID_IUnknown || iid == IID_IEnumFORMATETC) + { + *object = static_cast(this); + AddRef(); + return S_OK; + } + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE AddRef() override + { + return m_refs.fetch_add(1, std::memory_order_relaxed) + 1; + } + + ULONG STDMETHODCALLTYPE Release() override + { + const ULONG refs = m_refs.fetch_sub(1, std::memory_order_acq_rel) - 1; + if (!refs) + delete this; + return refs; + } + + HRESULT STDMETHODCALLTYPE Next(ULONG count, FORMATETC * formats, + ULONG * fetched) override + { + if (fetched) + *fetched = 0; + if (!formats || (count != 1 && !fetched)) + return E_INVALIDARG; + ULONG copied = 0; + while (copied < count && m_index < m_formats.size()) + formats[copied++] = m_formats[m_index++]; + if (fetched) + *fetched = copied; + return copied == count ? S_OK : S_FALSE; + } + + HRESULT STDMETHODCALLTYPE Skip(ULONG count) override + { + const size_t available = m_formats.size() - m_index; + const size_t skipped = (std::min)(available, count); + m_index += skipped; + return skipped == count ? S_OK : S_FALSE; + } + + HRESULT STDMETHODCALLTYPE Reset() override + { + m_index = 0; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE Clone(IEnumFORMATETC ** clone) override + { + if (!clone) + return E_POINTER; + *clone = nullptr; + try + { + *clone = new (std::nothrow) CFormatEnumerator(m_formats, m_index); + return *clone ? S_OK : E_OUTOFMEMORY; + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + catch (const std::length_error&) + { + return E_OUTOFMEMORY; + } + } + }; + + class CClipboardFileDataObject final : public IDataObject + { + private: + std::atomic m_refs { 1 }; + std::vector m_entries; + std::vector m_descriptors; + std::shared_ptr m_lease; + UINT m_fileDescriptor = 0; + UINT m_fileContents = 0; + UINT m_preferredDropEffect = 0; + + bool Matches(const FORMATETC& format, UINT clipboardFormat, + DWORD tymed) const + { + return format.cfFormat == clipboardFormat && + format.dwAspect == DVASPECT_CONTENT && format.lindex >= -1 && + (format.tymed & tymed); + } + + public: + CClipboardFileDataObject( + std::vector&& entries, + std::vector&& descriptors, + UINT fileDescriptor, UINT fileContents, + UINT preferredDropEffect) noexcept : + m_entries(std::move(entries)), + m_descriptors(std::move(descriptors)), + m_fileDescriptor(fileDescriptor), m_fileContents(fileContents), + m_preferredDropEffect(preferredDropEffect) + { + } + + void SetLease(std::shared_ptr lease) noexcept + { + m_lease = std::move(lease); + } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void ** object) + override + { + if (!object) + return E_POINTER; + *object = nullptr; + if (iid == IID_IUnknown || iid == IID_IDataObject) + { + *object = static_cast(this); + AddRef(); + return S_OK; + } + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE AddRef() override + { + return m_refs.fetch_add(1, std::memory_order_relaxed) + 1; + } + + ULONG STDMETHODCALLTYPE Release() override + { + const ULONG refs = m_refs.fetch_sub(1, std::memory_order_acq_rel) - 1; + if (!refs) + delete this; + return refs; + } + + HRESULT STDMETHODCALLTYPE GetData(FORMATETC * format, + STGMEDIUM * medium) override + { + if (!format || !medium) + return E_POINTER; + *medium = {}; + + if (Matches(*format, m_fileDescriptor, TYMED_HGLOBAL) && + format->lindex == -1) + { + if (m_descriptors.size() > + (std::numeric_limits::max)() || + m_descriptors.size() > ((std::numeric_limits::max)() - + offsetof(FILEGROUPDESCRIPTORW, fgd)) / + sizeof(FILEDESCRIPTORW)) + return E_OUTOFMEMORY; + const SIZE_T bytes = offsetof(FILEGROUPDESCRIPTORW, fgd) + + m_descriptors.size() * sizeof(FILEDESCRIPTORW); + HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, bytes); + if (!memory) + return E_OUTOFMEMORY; + FILEGROUPDESCRIPTORW * group = + static_cast(GlobalLock(memory)); + if (!group) + { + GlobalFree(memory); + return E_OUTOFMEMORY; + } + group->cItems = static_cast(m_descriptors.size()); + if (!m_descriptors.empty()) + memcpy(group->fgd, m_descriptors.data(), + m_descriptors.size() * sizeof(FILEDESCRIPTORW)); + GlobalUnlock(memory); + medium->tymed = TYMED_HGLOBAL; + medium->hGlobal = memory; + return S_OK; + } + + if (Matches(*format, m_fileContents, TYMED_ISTREAM)) + { + if (format->lindex < 0 || + static_cast(format->lindex) >= m_entries.size()) + return DV_E_LINDEX; + const ClipboardRemoteFileEntry& entry = + m_entries[static_cast(format->lindex)]; + if (entry.type != KVMFR_CLIPBOARD_FILE_TYPE_REGULAR) + return DV_E_LINDEX; + try + { + CClipboardFileStream * stream = new (std::nothrow) + CClipboardFileStream(entry, m_lease); + if (!stream) + return E_OUTOFMEMORY; + medium->tymed = TYMED_ISTREAM; + medium->pstm = stream; + return S_OK; + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + catch (const std::length_error&) + { + return E_OUTOFMEMORY; + } + } + + if (Matches(*format, m_preferredDropEffect, TYMED_HGLOBAL) && + format->lindex == -1) + { + HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD)); + if (!memory) + return E_OUTOFMEMORY; + DWORD * effect = static_cast(GlobalLock(memory)); + if (!effect) + { + GlobalFree(memory); + return E_OUTOFMEMORY; + } + *effect = DROPEFFECT_COPY; + GlobalUnlock(memory); + medium->tymed = TYMED_HGLOBAL; + medium->hGlobal = memory; + return S_OK; + } + return DV_E_FORMATETC; + } + + HRESULT STDMETHODCALLTYPE GetDataHere(FORMATETC *, STGMEDIUM *) override + { + return DATA_E_FORMATETC; + } + + HRESULT STDMETHODCALLTYPE QueryGetData(FORMATETC * format) override + { + if (!format) + return E_POINTER; + if (Matches(*format, m_fileDescriptor, TYMED_HGLOBAL) && + format->lindex == -1) + return S_OK; + if (Matches(*format, m_preferredDropEffect, TYMED_HGLOBAL) && + format->lindex == -1) + return S_OK; + if (Matches(*format, m_fileContents, TYMED_ISTREAM)) + { + if (format->lindex == -1) + for (const ClipboardRemoteFileEntry& entry : m_entries) + if (entry.type == KVMFR_CLIPBOARD_FILE_TYPE_REGULAR) + return S_OK; + if (format->lindex >= 0 && + static_cast(format->lindex) < m_entries.size() && + m_entries[static_cast(format->lindex)].type == + KVMFR_CLIPBOARD_FILE_TYPE_REGULAR) + return S_OK; + } + return DV_E_FORMATETC; + } + + HRESULT STDMETHODCALLTYPE GetCanonicalFormatEtc(FORMATETC * input, + FORMATETC * output) override + { + if (!input || !output) + return E_POINTER; + *output = *input; + output->ptd = nullptr; + return DATA_S_SAMEFORMATETC; + } + + HRESULT STDMETHODCALLTYPE SetData(FORMATETC *, STGMEDIUM *, BOOL) + override + { + return E_NOTIMPL; + } + + HRESULT STDMETHODCALLTYPE EnumFormatEtc(DWORD direction, + IEnumFORMATETC ** enumerator) override + { + if (!enumerator) + return E_POINTER; + *enumerator = nullptr; + if (direction != DATADIR_GET) + return E_NOTIMPL; + try + { + std::vector formats; + formats.reserve(3U); + formats.push_back({ static_cast(m_fileDescriptor), + nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }); + for (const ClipboardRemoteFileEntry& entry : m_entries) + if (entry.type == KVMFR_CLIPBOARD_FILE_TYPE_REGULAR) + { + formats.push_back({ static_cast(m_fileContents), + nullptr, DVASPECT_CONTENT, -1, TYMED_ISTREAM }); + break; + } + formats.push_back({ + static_cast(m_preferredDropEffect), nullptr, + DVASPECT_CONTENT, -1, TYMED_HGLOBAL }); + *enumerator = new (std::nothrow) + CFormatEnumerator(std::move(formats)); + return *enumerator ? S_OK : E_OUTOFMEMORY; + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + catch (const std::length_error&) + { + return E_OUTOFMEMORY; + } + } + + HRESULT STDMETHODCALLTYPE DAdvise(FORMATETC *, DWORD, IAdviseSink *, + DWORD *) override { return OLE_E_ADVISENOTSUPPORTED; } + HRESULT STDMETHODCALLTYPE DUnadvise(DWORD) override + { return OLE_E_ADVISENOTSUPPORTED; } + HRESULT STDMETHODCALLTYPE EnumDAdvise(IEnumSTATDATA **) override + { return OLE_E_ADVISENOTSUPPORTED; } + }; +} + +CLocalClipboardFiles::CLocalClipboardFiles(HANDLE userToken) : + m_userToken(userToken) +{ +} + +CLocalClipboardFiles::~CLocalClipboardFiles() +{ + if (m_userToken) + CloseHandle(m_userToken); +} + +std::shared_ptr CLocalClipboardFiles::Capture( + HDROP drop, KVMFRClipboardFileError& error) +{ + error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID; + if (!drop) + { + SetLastError(ERROR_INVALID_HANDLE); + return nullptr; + } + std::vector roots; + HANDLE userToken = nullptr; + std::shared_ptr dataset; + try + { + if (!CaptureUserToken(userToken, error)) + return nullptr; + CLocalClipboardFiles * raw = new CLocalClipboardFiles(userToken); + userToken = nullptr; + dataset = std::shared_ptr(raw); + + const UINT count = DragQueryFileW(drop, 0xffffffffU, nullptr, 0); + if (!count) + { + SetLastError(ERROR_INVALID_DATA); + return nullptr; + } + roots.reserve(count); + for (UINT index = 0; index < count; ++index) + { + const UINT length = DragQueryFileW(drop, index, nullptr, 0); + if (!length) + { + SetLastError(ERROR_INVALID_DATA); + return nullptr; + } + std::vector path(static_cast(length) + 1U); + std::wstring source; + if (DragQueryFileW(drop, index, path.data(), length + 1U) != length) + { + SetLastError(ERROR_INVALID_DATA); + error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID; + return nullptr; + } + if (!SourcePath(path.data(), source)) + { + error = FileError(GetLastError()); + return nullptr; + } + roots.emplace_back(std::move(source)); + } + + CThreadImpersonation impersonation(dataset->m_userToken); + if (!impersonation.Active()) + { + error = TokenError(GetLastError()); + return nullptr; + } + for (const std::wstring& root : roots) + if (!dataset->AddRoot(root, error)) + return nullptr; + if (!impersonation.Finish()) + { + error = TokenError(GetLastError()); + return nullptr; + } + } + catch (const std::bad_alloc&) + { + if (userToken) + CloseHandle(userToken); + SetLastError(ERROR_OUTOFMEMORY); + error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY; + return nullptr; + } + error = KVMFR_CLIPBOARD_FILE_ERROR_NONE; + return dataset; +} + +size_t CLocalClipboardFiles::RootCount() const +{ + // Roots are immutable after Capture publishes the dataset. + return m_roots.size(); +} + +bool CLocalClipboardFiles::AddRoot(const std::wstring& path, + KVMFRClipboardFileError& error) +{ + Node node; + try + { + node.path = path; + node.name = BaseName(path); + } + catch (const std::bad_alloc&) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY; + return false; + } + if (!ValidComponent(node.name)) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED; + return false; + } + if (!CaptureIdentity(node, error)) + return false; + for (uint64_t root : m_roots) + if (SameComponent(m_nodes[static_cast(root - 1U)].name, + node.name)) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID; + return false; + } + + node.id = static_cast(m_nodes.size()) + 1U; + m_nodes.emplace_back(std::move(node)); + m_roots.push_back(static_cast(m_nodes.size())); + return true; +} + +bool CLocalClipboardFiles::RefreshRoots(KVMFRClipboardFileError& error) +{ + std::vector refreshed; + try + { + refreshed.reserve(m_roots.size()); + for (uint64_t id : m_roots) + { + const Node& source = m_nodes[static_cast(id - 1U)]; + Node node; + node.id = source.id; + node.path = source.path; + node.name = source.name; + if (!CaptureIdentity(node, error)) + return false; + refreshed.emplace_back(std::move(node)); + } + } + catch (const std::bad_alloc&) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY; + return false; + } + + for (size_t index = 0; index < refreshed.size(); ++index) + { + Node& target = m_nodes[static_cast(m_roots[index] - 1U)]; + Node& source = refreshed[index]; + target.size = source.size; + target.createdNs = source.createdNs; + target.modifiedNs = source.modifiedNs; + target.type = source.type; + target.volumeSerial = source.volumeSerial; + target.fileIndexHigh = source.fileIndexHigh; + target.fileIndexLow = source.fileIndexLow; + } + return true; +} + +bool CLocalClipboardFiles::CaptureIdentity(Node& node, + KVMFRClipboardFileError& error) const +{ + const DWORD flags = FILE_FLAG_OPEN_REPARSE_POINT | + FILE_FLAG_BACKUP_SEMANTICS; + HANDLE file = CreateFileW(node.path.c_str(), FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, flags, nullptr); + if (file == INVALID_HANDLE_VALUE) + { + error = FileError(GetLastError()); + return false; + } + FILE_ATTRIBUTE_TAG_INFO tag = {}; + BY_HANDLE_FILE_INFORMATION information = {}; + bool success = GetFileInformationByHandle(file, &information) != FALSE; + if (success && + (information.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) + success = GetFileInformationByHandleEx(file, + FileAttributeTagInfo, &tag, sizeof(tag)) != FALSE; + const DWORD winError = success ? ERROR_SUCCESS : GetLastError(); + CloseHandle(file); + if (!success) + { + error = FileError(winError); + return false; + } + const DWORD attributes = tag.FileAttributes | + information.dwFileAttributes; + if ((attributes & FILE_ATTRIBUTE_DEVICE) || + ((attributes & FILE_ATTRIBUTE_REPARSE_POINT) && + IsReparseTagNameSurrogate(tag.ReparseTag))) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED; + return false; + } + node.type = information.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ? + KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY : + KVMFR_CLIPBOARD_FILE_TYPE_REGULAR; + node.size = node.type == KVMFR_CLIPBOARD_FILE_TYPE_REGULAR ? + (static_cast(information.nFileSizeHigh) << 32) | + information.nFileSizeLow : 0; + node.createdNs = FileTimeToUnixNs(information.ftCreationTime); + node.modifiedNs = FileTimeToUnixNs(information.ftLastWriteTime); + node.volumeSerial = information.dwVolumeSerialNumber; + node.fileIndexHigh = information.nFileIndexHigh; + node.fileIndexLow = information.nFileIndexLow; + return true; +} + +bool CLocalClipboardFiles::CheckIdentity(const Node& node, + KVMFRClipboardFileError& error) const +{ + Node current; + try + { + current.path = node.path; + } + catch (const std::bad_alloc&) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY; + return false; + } + if (!CaptureIdentity(current, error)) + return false; + if (current.volumeSerial != node.volumeSerial || + current.fileIndexHigh != node.fileIndexHigh || + current.fileIndexLow != node.fileIndexLow || + current.type != node.type || current.size != node.size || + current.createdNs != node.createdNs || + current.modifiedNs != node.modifiedNs) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_STALE; + return false; + } + return true; +} + +bool CLocalClipboardFiles::LoadChildren(size_t index, + KVMFRClipboardFileError& error) +{ + if (index >= m_nodes.size()) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_NOT_FOUND; + return false; + } + if (m_nodes[index].type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY; + return false; + } + if (m_nodes[index].childrenLoaded) + return true; + + if (!CheckIdentity(m_nodes[index], error)) + return false; + + HANDLE search = INVALID_HANDLE_VALUE; + try + { + const std::wstring parent = m_nodes[index].path; + const std::wstring pattern = JoinPath(parent, L"*"); + WIN32_FIND_DATAW found = {}; + search = FindFirstFileExW(pattern.c_str(), FindExInfoBasic, + &found, FindExSearchNameMatch, nullptr, 0); + if (search == INVALID_HANDLE_VALUE) + { + const DWORD winError = GetLastError(); + if (winError == ERROR_FILE_NOT_FOUND && + CheckIdentity(m_nodes[index], error)) + { + m_nodes[index].childrenLoaded = true; + return true; + } + if (!error) + error = FileError(winError); + return false; + } + + std::vector children; + do + { + if (!wcscmp(found.cFileName, L".") || + !wcscmp(found.cFileName, L"..")) + continue; + if (!ValidComponent(found.cFileName)) + { + FindClose(search); + search = INVALID_HANDLE_VALUE; + error = KVMFR_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED; + return false; + } + Node child; + child.path = JoinPath(parent, found.cFileName); + child.name = found.cFileName; + if (!CaptureIdentity(child, error)) + { + FindClose(search); + search = INVALID_HANDLE_VALUE; + return false; + } + children.emplace_back(std::move(child)); + } + while (FindNextFileW(search, &found)); + const DWORD enumerationError = GetLastError(); + FindClose(search); + search = INVALID_HANDLE_VALUE; + if (enumerationError != ERROR_NO_MORE_FILES) + { + error = FileError(enumerationError); + return false; + } + if (!CheckIdentity(m_nodes[index], error)) + return false; + + const size_t first = m_nodes.size(); + if (children.size() > m_nodes.max_size() - first) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY; + return false; + } + m_nodes.reserve(first + children.size()); + std::vector& ids = m_nodes[index].children; + ids.reserve(children.size()); + for (Node& child : children) + { + child.id = static_cast(m_nodes.size()) + 1U; + ids.push_back(child.id); + m_nodes.emplace_back(std::move(child)); + } + m_nodes[index].childrenLoaded = true; + } + catch (const std::bad_alloc&) + { + if (search != INVALID_HANDLE_VALUE) + FindClose(search); + error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY; + return false; + } + return true; +} + +bool CLocalClipboardFiles::List(uint64_t node, std::vector& data, + KVMFRClipboardFileError& error) +{ + std::lock_guard lock(m_lock); + error = KVMFR_CLIPBOARD_FILE_ERROR_NONE; + const std::vector * children = nullptr; + if (node == KVMFR_CLIPBOARD_FILE_ROOT_NODE) + { + if (!m_published) + { + CThreadImpersonation impersonation(m_userToken); + if (!impersonation.Active()) + { + error = TokenError(GetLastError()); + return false; + } + if (!RefreshRoots(error)) + return false; + if (!impersonation.Finish()) + { + error = TokenError(GetLastError()); + return false; + } + m_published = true; + } + children = &m_roots; + } + else + { + if (!m_published) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID; + return false; + } + if (node > m_nodes.size()) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_NOT_FOUND; + return false; + } + const size_t index = static_cast(node - 1U); + CThreadImpersonation impersonation(m_userToken); + if (!impersonation.Active()) + { + error = TokenError(GetLastError()); + return false; + } + if (!LoadChildren(index, error)) + return false; + if (!impersonation.Finish()) + { + error = TokenError(GetLastError()); + return false; + } + children = &m_nodes[index].children; + } + + data.clear(); + try + { + for (uint64_t id : *children) + { + const Node& child = m_nodes[static_cast(id - 1U)]; + std::vector name; + if (!ToUTF8(child.name, name)) + { + error = FileError(GetLastError()); + return false; + } + if (name.size() > (std::numeric_limits::max)()) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID; + return false; + } + const uint64_t recordBytes = KVMFR_CLIPBOARD_FILE_ENTRY_BYTES( + name.size()); + if (recordBytes > (std::numeric_limits::max)() || + data.size() > (std::numeric_limits::max)() - + static_cast(recordBytes)) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY; + return false; + } + const size_t offset = data.size(); + data.resize(offset + static_cast(recordBytes), 0); + KVMFRClipboardFileEntry entry = {}; + entry.node = child.id; + entry.size = child.size; + entry.createdNs = child.createdNs; + entry.modifiedNs = child.modifiedNs; + entry.type = child.type; + entry.nameLength = static_cast(name.size()); + memcpy(data.data() + offset, &entry, sizeof(entry)); + memcpy(data.data() + offset + sizeof(entry), name.data(), name.size()); + } + } + catch (const std::bad_alloc&) + { + data.clear(); + error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY; + return false; + } + return true; +} + +bool CLocalClipboardFiles::Read(uint64_t node, uint64_t offset, + uint32_t length, std::vector& data, + KVMFRClipboardFileError& error) +{ + error = KVMFR_CLIPBOARD_FILE_ERROR_NONE; + std::wstring path; + Node expected; + { + std::lock_guard lock(m_lock); + if (!m_published || !node || node > m_nodes.size()) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_NOT_FOUND; + return false; + } + const Node& source = m_nodes[static_cast(node - 1U)]; + if (source.type != KVMFR_CLIPBOARD_FILE_TYPE_REGULAR) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_IS_DIRECTORY; + return false; + } + try + { + path = source.path; + expected.id = source.id; + expected.size = source.size; + expected.createdNs = source.createdNs; + expected.modifiedNs = source.modifiedNs; + expected.type = source.type; + expected.volumeSerial = source.volumeSerial; + expected.fileIndexHigh = source.fileIndexHigh; + expected.fileIndexLow = source.fileIndexLow; + } + catch (const std::bad_alloc&) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY; + return false; + } + } + if (length > KVMFR_CLIPBOARD_FILE_READ_BYTES || + offset > static_cast( + (std::numeric_limits::max)()) || + offset > (std::numeric_limits::max)() - length) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID; + return false; + } + + CThreadImpersonation impersonation(m_userToken); + if (!impersonation.Active()) + { + error = TokenError(GetLastError()); + return false; + } + + HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_RANDOM_ACCESS, nullptr); + if (file == INVALID_HANDLE_VALUE) + { + error = FileError(GetLastError()); + return false; + } + FILE_STANDARD_INFO standard = {}; + FILE_ATTRIBUTE_TAG_INFO tag = {}; + BY_HANDLE_FILE_INFORMATION identity = {}; + if (!GetFileInformationByHandleEx(file, FileStandardInfo, &standard, + sizeof(standard)) || + !GetFileInformationByHandleEx(file, FileAttributeTagInfo, &tag, + sizeof(tag)) || + !GetFileInformationByHandle(file, &identity)) + { + error = FileError(GetLastError()); + CloseHandle(file); + return false; + } + if (standard.Directory || (tag.FileAttributes & FILE_ATTRIBUTE_DEVICE) || + ((tag.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) && + IsReparseTagNameSurrogate(tag.ReparseTag))) + { + error = standard.Directory ? KVMFR_CLIPBOARD_FILE_ERROR_IS_DIRECTORY : + KVMFR_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED; + CloseHandle(file); + return false; + } + { + std::lock_guard lock(m_lock); + if (!node || node > m_nodes.size()) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_STALE; + CloseHandle(file); + return false; + } + const Node& source = m_nodes[static_cast(node - 1U)]; + if (!SameIdentity(source, identity)) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_STALE; + CloseHandle(file); + return false; + } + } + if (standard.EndOfFile.QuadPart < 0) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_IO; + CloseHandle(file); + return false; + } + const uint64_t actualSize = static_cast( + standard.EndOfFile.QuadPart); + if (!SameIdentity(expected, identity) || actualSize != expected.size) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_STALE; + CloseHandle(file); + return false; + } + + const uint32_t wanted = offset >= actualSize ? 0 : + static_cast((std::min)(length, actualSize - offset)); + try + { + data.resize(wanted); + } + catch (const std::bad_alloc&) + { + error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY; + CloseHandle(file); + return false; + } + LARGE_INTEGER position = {}; + position.QuadPart = offset; + DWORD read = 0; + bool success = SetFilePointerEx(file, position, nullptr, FILE_BEGIN) && + (!wanted || ReadFile(file, data.data(), wanted, &read, nullptr)); + DWORD ioError = success ? ERROR_SUCCESS : GetLastError(); + BY_HANDLE_FILE_INFORMATION after = {}; + if (success && !GetFileInformationByHandle(file, &after)) + { + success = false; + ioError = GetLastError(); + } + if (success && !SameIdentity(expected, after)) + { + success = false; + error = KVMFR_CLIPBOARD_FILE_ERROR_STALE; + } + CloseHandle(file); + if (!success || read != wanted) + { + data.clear(); + if (error == KVMFR_CLIPBOARD_FILE_ERROR_NONE) + error = FileError(ioError ? ioError : ERROR_READ_FAULT); + return false; + } + if (!impersonation.Finish()) + { + data.clear(); + error = TokenError(GetLastError()); + return false; + } + return true; +} + +HRESULT ParseClipboardFileList(uint64_t parent, const uint8_t * data, + size_t length, std::vector& entries) +{ + if ((!data && length) || (length && length < sizeof(KVMFRClipboardFileEntry))) + return HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + size_t offset = 0; + try + { + while (offset < length) + { + if (length - offset < sizeof(KVMFRClipboardFileEntry)) + return HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + KVMFRClipboardFileEntry wire = {}; + memcpy(&wire, data + offset, sizeof(wire)); + const uint64_t bytes = KVMFR_CLIPBOARD_FILE_ENTRY_BYTES(wire.nameLength); + if (!wire.node || !wire.nameLength || + (wire.type != KVMFR_CLIPBOARD_FILE_TYPE_REGULAR && + wire.type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY) || + bytes > length - offset || + wire.nameLength > bytes - sizeof(wire)) + return HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + std::wstring name; + if (!FromUTF8(data + offset + sizeof(wire), wire.nameLength, name) || + !ValidComponent(name)) + return HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + for (size_t pad = sizeof(wire) + wire.nameLength; + pad < static_cast(bytes); ++pad) + if (data[offset + pad]) + return HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + ClipboardRemoteFileEntry entry; + entry.node = wire.node; + entry.parent = parent; + entry.size = wire.type == KVMFR_CLIPBOARD_FILE_TYPE_REGULAR ? + wire.size : 0; + entry.createdNs = wire.createdNs; + entry.modifiedNs = wire.modifiedNs; + entry.type = wire.type; + entry.name = std::move(name); + entries.emplace_back(std::move(entry)); + offset += static_cast(bytes); + } + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + catch (const std::length_error&) + { + return E_OUTOFMEMORY; + } + return S_OK; +} + +HRESULT CreateClipboardFileDataObject(uint64_t dataset, uint64_t acquisition, + std::vector&& entries, + std::shared_ptr provider, + IDataObject ** object) +{ + if (!object) + return E_POINTER; + *object = nullptr; + if (!dataset || !acquisition || !provider || entries.empty()) + return E_INVALIDARG; + + try + { + if (entries.size() > + static_cast((std::numeric_limits::max)()) || + entries.size() > + static_cast((std::numeric_limits::max)())) + return HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + + std::unordered_map byID; + std::map> siblings; + byID.reserve(entries.size()); + for (size_t index = 0; index < entries.size(); ++index) + { + const ClipboardRemoteFileEntry& entry = entries[index]; + if (!entry.node || !ValidComponent(entry.name) || + !byID.emplace(entry.node, index).second || + !siblings[entry.parent].insert(entry.name).second) + return HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + } + + std::vector paths(entries.size()); + std::vector state(entries.size(), 0); + std::vector chain; + chain.reserve((std::min)(entries.size(), MAX_PATH)); + for (size_t start = 0; start < entries.size(); ++start) + { + if (state[start] == 2) + continue; + chain.clear(); + size_t current = start; + while (state[current] != 2) + { + if (state[current] == 1 || chain.size() >= MAX_PATH) + return HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + state[current] = 1; + chain.push_back(current); + const ClipboardRemoteFileEntry& entry = entries[current]; + if (!entry.parent) + break; + const auto parent = byID.find(entry.parent); + if (parent == byID.end() || + entries[parent->second].type != + KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY) + return HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + current = parent->second; + } + while (!chain.empty()) + { + const size_t index = chain.back(); + chain.pop_back(); + const ClipboardRemoteFileEntry& entry = entries[index]; + if (!entry.parent) + paths[index] = entry.name; + else + { + const size_t parent = byID.find(entry.parent)->second; + if (state[parent] != 2) + return HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + paths[index] = paths[parent] + L"\\" + entry.name; + } + if (paths[index].size() >= MAX_PATH) + return HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + state[index] = 2; + } + } + + std::vector descriptors; + if (entries.size() > descriptors.max_size()) + return E_OUTOFMEMORY; + descriptors.resize(entries.size()); + for (size_t index = 0; index < entries.size(); ++index) + { + const ClipboardRemoteFileEntry& entry = entries[index]; + FILEDESCRIPTORW& descriptor = descriptors[index]; + descriptor.dwFlags = FD_ATTRIBUTES | FD_UNICODE | FD_PROGRESSUI; + descriptor.dwFileAttributes = entry.type == + KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? FILE_ATTRIBUTE_DIRECTORY : + FILE_ATTRIBUTE_NORMAL; + if (entry.createdNs) + { + descriptor.dwFlags |= FD_CREATETIME; + descriptor.ftCreationTime = UnixNsToFileTime(entry.createdNs); + } + if (entry.modifiedNs) + { + descriptor.dwFlags |= FD_WRITESTIME; + descriptor.ftLastWriteTime = UnixNsToFileTime(entry.modifiedNs); + } + if (entry.type == KVMFR_CLIPBOARD_FILE_TYPE_REGULAR) + { + descriptor.dwFlags |= FD_FILESIZE; + descriptor.nFileSizeHigh = static_cast(entry.size >> 32); + descriptor.nFileSizeLow = static_cast(entry.size); + } + if (FAILED(StringCchCopyW(descriptor.cFileName, + ARRAYSIZE(descriptor.cFileName), paths[index].c_str()))) + return HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + } + + const UINT fileDescriptor = + RegisterClipboardFormatW(CFSTR_FILEDESCRIPTORW); + const UINT fileContents = RegisterClipboardFormatW(CFSTR_FILECONTENTS); + const UINT preferredDropEffect = + RegisterClipboardFormatW(CFSTR_PREFERREDDROPEFFECT); + if (!fileDescriptor || !fileContents || !preferredDropEffect) + return HRESULT_FROM_WIN32(GetLastError() ? GetLastError() : + ERROR_INVALID_DATA); + + CClipboardFileDataObject * result = new (std::nothrow) + CClipboardFileDataObject(std::move(entries), + std::move(descriptors), fileDescriptor, fileContents, + preferredDropEffect); + if (!result) + return E_OUTOFMEMORY; + try + { + std::shared_ptr lease = + std::make_shared(dataset, acquisition, + std::move(provider)); + result->SetLease(std::move(lease)); + } + catch (const std::bad_alloc&) + { + result->Release(); + return E_OUTOFMEMORY; + } + *object = result; + return S_OK; + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + catch (const std::length_error&) + { + return E_OUTOFMEMORY; + } +} diff --git a/idd/LGIddHelper/CClipboardFiles.h b/idd/LGIddHelper/CClipboardFiles.h new file mode 100644 index 00000000..4dea7fbb --- /dev/null +++ b/idd/LGIddHelper/CClipboardFiles.h @@ -0,0 +1,115 @@ +/** + * 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 + */ + +#pragma once + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +struct ClipboardRemoteFileEntry +{ + uint64_t node = 0; + uint64_t parent = 0; + uint64_t size = 0; + uint64_t createdNs = 0; + uint64_t modifiedNs = 0; + KVMFRClipboardFileType type = KVMFR_CLIPBOARD_FILE_TYPE_REGULAR; + std::wstring name; +}; + +class CLocalClipboardFiles final +{ +public: + struct Node + { + uint64_t id = 0; + uint64_t size = 0; + uint64_t createdNs = 0; + uint64_t modifiedNs = 0; + KVMFRClipboardFileType type = KVMFR_CLIPBOARD_FILE_TYPE_REGULAR; + std::wstring name; + std::wstring path; + bool childrenLoaded = false; + DWORD volumeSerial = 0; + DWORD fileIndexHigh = 0; + DWORD fileIndexLow = 0; + std::vector children; + }; + +private: + std::mutex m_lock; + std::vector m_nodes; + std::vector m_roots; + HANDLE m_userToken = nullptr; + bool m_published = false; + + explicit CLocalClipboardFiles(HANDLE userToken); + bool AddRoot(const std::wstring& path, KVMFRClipboardFileError& error); + bool RefreshRoots(KVMFRClipboardFileError& error); + bool LoadChildren(size_t index, KVMFRClipboardFileError& error); + bool CaptureIdentity(Node& node, KVMFRClipboardFileError& error) const; + bool CheckIdentity(const Node& node, KVMFRClipboardFileError& error) const; + +public: + ~CLocalClipboardFiles(); + + CLocalClipboardFiles(const CLocalClipboardFiles&) = delete; + CLocalClipboardFiles& operator=(const CLocalClipboardFiles&) = delete; + + static std::shared_ptr Capture(HDROP drop, + KVMFRClipboardFileError& error); + + size_t RootCount() const; + bool List(uint64_t node, std::vector& data, + KVMFRClipboardFileError& error); + bool Read(uint64_t node, uint64_t offset, uint32_t length, + std::vector& data, KVMFRClipboardFileError& error); +}; + +class IRemoteClipboardFileProvider +{ +public: + virtual ~IRemoteClipboardFileProvider() = default; + + virtual HRESULT ReadClipboardFile(uint64_t dataset, uint64_t acquisition, + uint64_t node, uint64_t offset, void * data, ULONG length, + ULONG& read) = 0; + virtual void ReleaseClipboardFileDataset(uint64_t dataset, + uint64_t acquisition) = 0; +}; + +HRESULT ParseClipboardFileList(uint64_t parent, const uint8_t * data, + size_t length, std::vector& entries); + +HRESULT CreateClipboardFileDataObject(uint64_t dataset, uint64_t acquisition, + std::vector&& entries, + std::shared_ptr provider, + IDataObject ** object); diff --git a/idd/LGIddHelper/LGIddHelper.vcxproj b/idd/LGIddHelper/LGIddHelper.vcxproj index eb11a96d..87f6ed0a 100644 --- a/idd/LGIddHelper/LGIddHelper.vcxproj +++ b/idd/LGIddHelper/LGIddHelper.vcxproj @@ -187,10 +187,14 @@ copy /Y "$(ProjectDir)VERSION" "$(SolutionDir)$(Platform)\$(Configuration)\LGIdd false StdCall + + %(AdditionalDependencies);Ole32.lib;Shell32.lib;Wtsapi32.lib + + @@ -208,6 +212,7 @@ copy /Y "$(ProjectDir)VERSION" "$(SolutionDir)$(Platform)\$(Configuration)\LGIdd + diff --git a/idd/LGIddHelper/LGIddHelper.vcxproj.filters b/idd/LGIddHelper/LGIddHelper.vcxproj.filters index e4f10bd1..d1114600 100644 --- a/idd/LGIddHelper/LGIddHelper.vcxproj.filters +++ b/idd/LGIddHelper/LGIddHelper.vcxproj.filters @@ -60,6 +60,9 @@ Source Files + + Source Files + @@ -104,6 +107,9 @@ Header Files + + Header Files + Header Files