mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-22 07:01:30 +00:00
Add two protocol-sized read-ahead buffers to each active clipboard file stream. Fetch the next remote block on a worker while the caller or CopyTo destination consumes the current block. Serialize stream operations and invalidate queued work on seeks and failures. Join the worker before releasing the remote dataset lease.
2184 lines
63 KiB
C++
2184 lines
63 KiB
C++
/**
|
|
* Looking Glass
|
|
* Copyright © 2017-2026 The Looking Glass Authors
|
|
* https://looking-glass.io
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the Free
|
|
* Software Foundation; either version 2 of the License, or (at your option)
|
|
* any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc., 59
|
|
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
|
|
#include "CClipboardFiles.h"
|
|
|
|
#include <ShlObj.h>
|
|
#include <strsafe.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <condition_variable>
|
|
#include <cstring>
|
|
#include <cwchar>
|
|
#include <exception>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <new>
|
|
#include <set>
|
|
#include <stdexcept>
|
|
#include <system_error>
|
|
#include <thread>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
|
|
namespace
|
|
{
|
|
static constexpr uint64_t WINDOWS_EPOCH_TICKS =
|
|
UINT64_C(116444736000000000);
|
|
static constexpr size_t COPY_BUFFER_BYTES =
|
|
KVMFR_CLIPBOARD_FILE_READ_BYTES;
|
|
static constexpr ULONG FILE_READ_BYTES =
|
|
static_cast<ULONG>(KVMFR_CLIPBOARD_FILE_READ_BYTES);
|
|
|
|
class CThreadImpersonation final
|
|
{
|
|
private:
|
|
HANDLE m_previousToken = nullptr;
|
|
bool m_active = false;
|
|
DWORD m_error = ERROR_SUCCESS;
|
|
|
|
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;
|
|
m_error = error;
|
|
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)
|
|
{
|
|
m_error = error;
|
|
return;
|
|
}
|
|
SetLastError(ERROR_SUCCESS);
|
|
}
|
|
|
|
if (SetThreadToken(nullptr, token))
|
|
m_active = true;
|
|
else
|
|
{
|
|
const DWORD error = GetLastError();
|
|
ClosePreviousToken();
|
|
m_error = error;
|
|
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;
|
|
}
|
|
|
|
DWORD Error() const
|
|
{
|
|
return m_error;
|
|
}
|
|
|
|
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, DWORD& winError)
|
|
{
|
|
token = nullptr;
|
|
winError = ERROR_SUCCESS;
|
|
HANDLE processToken = nullptr;
|
|
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE,
|
|
&processToken))
|
|
{
|
|
winError = GetLastError();
|
|
error = TokenError(winError);
|
|
return false;
|
|
}
|
|
|
|
const bool duplicated = DuplicateTokenEx(processToken,
|
|
TOKEN_QUERY | TOKEN_IMPERSONATE, nullptr, SecurityImpersonation,
|
|
TokenImpersonation, &token) != FALSE;
|
|
const DWORD duplicateError = duplicated ? ERROR_SUCCESS : GetLastError();
|
|
CloseHandle(processToken);
|
|
if (!duplicated)
|
|
{
|
|
winError = 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<uint64_t>::max)() / 100U ?
|
|
ticks * 100U : (std::numeric_limits<uint64_t>::max)();
|
|
}
|
|
|
|
FILETIME UnixNsToFileTime(uint64_t ns)
|
|
{
|
|
ULARGE_INTEGER value = {};
|
|
const uint64_t ticks = ns / 100U;
|
|
value.QuadPart = ticks <=
|
|
(std::numeric_limits<uint64_t>::max)() - WINDOWS_EPOCH_TICKS ?
|
|
ticks + WINDOWS_EPOCH_TICKS :
|
|
(std::numeric_limits<uint64_t>::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<uint64_t>(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<uint8_t>& output)
|
|
{
|
|
if (text.empty() || text.size() >
|
|
static_cast<size_t>((std::numeric_limits<int>::max)()))
|
|
{
|
|
SetLastError(ERROR_INVALID_DATA);
|
|
return false;
|
|
}
|
|
const int count = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS,
|
|
text.data(), static_cast<int>(text.size()), nullptr, 0, nullptr, nullptr);
|
|
if (count <= 0)
|
|
return false;
|
|
try
|
|
{
|
|
const size_t offset = output.size();
|
|
output.resize(offset + static_cast<size_t>(count));
|
|
if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS,
|
|
text.data(), static_cast<int>(text.size()),
|
|
reinterpret_cast<char *>(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<size_t>((std::numeric_limits<int>::max)()))
|
|
return false;
|
|
const int count = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
|
|
reinterpret_cast<const char *>(data), static_cast<int>(length),
|
|
nullptr, 0);
|
|
if (count <= 0)
|
|
return false;
|
|
try
|
|
{
|
|
output.resize(static_cast<size_t>(count));
|
|
}
|
|
catch (const std::bad_alloc&)
|
|
{
|
|
return false;
|
|
}
|
|
return MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
|
|
reinterpret_cast<const char *>(data), static_cast<int>(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<wchar_t>(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<int>(left.size()), right.c_str(),
|
|
static_cast<int>(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<IRemoteClipboardFileProvider> provider;
|
|
|
|
CClipboardFileDatasetLease(uint64_t dataset, uint64_t acquisition,
|
|
std::shared_ptr<IRemoteClipboardFileProvider> provider) :
|
|
dataset(dataset), acquisition(acquisition), provider(std::move(provider))
|
|
{
|
|
}
|
|
|
|
~CClipboardFileDatasetLease()
|
|
{
|
|
provider->ReleaseClipboardFileDataset(dataset, acquisition);
|
|
}
|
|
};
|
|
|
|
class CClipboardFileStream final : public IStream
|
|
{
|
|
private:
|
|
static constexpr size_t PREFETCH_BLOCKS = 2U;
|
|
|
|
enum class PrefetchState
|
|
{
|
|
EMPTY,
|
|
QUEUED,
|
|
FILLING,
|
|
READY
|
|
};
|
|
|
|
struct PrefetchBlock
|
|
{
|
|
std::vector<uint8_t> data;
|
|
uint64_t offset = 0;
|
|
ULONG length = 0;
|
|
HRESULT result = S_OK;
|
|
uint64_t generation = 0;
|
|
PrefetchState state = PrefetchState::EMPTY;
|
|
};
|
|
|
|
std::atomic<ULONG> m_refs { 1 };
|
|
ClipboardRemoteFileEntry m_entry;
|
|
std::shared_ptr<CClipboardFileDatasetLease> m_lease;
|
|
std::mutex m_operationLock;
|
|
std::mutex m_lock;
|
|
std::condition_variable m_prefetchWork;
|
|
std::condition_variable m_prefetchReady;
|
|
// The consumer can drain one protocol-sized block while the worker fills
|
|
// the other, bounding each active stream to two MiB of read-ahead.
|
|
std::array<PrefetchBlock, PREFETCH_BLOCKS> m_prefetch;
|
|
std::thread m_prefetchThread;
|
|
uint64_t m_offset = 0;
|
|
uint64_t m_prefetchGeneration = 1;
|
|
bool m_prefetchStarted = false;
|
|
bool m_prefetchStop = false;
|
|
|
|
HRESULT ReadProvider(uint64_t offset, void * data, ULONG length)
|
|
{
|
|
ULONG actual = 0;
|
|
const HRESULT result = m_lease->provider->ReadClipboardFile(
|
|
m_lease->dataset, m_lease->acquisition,
|
|
m_entry.node, offset, data, length, actual);
|
|
if (FAILED(result))
|
|
return result;
|
|
return actual == length ? S_OK : STG_E_READFAULT;
|
|
}
|
|
|
|
bool Contains(const PrefetchBlock& block, uint64_t offset) const
|
|
{
|
|
return block.generation == m_prefetchGeneration &&
|
|
block.state != PrefetchState::EMPTY &&
|
|
offset >= block.offset && offset - block.offset < block.length;
|
|
}
|
|
|
|
void InvalidatePrefetchLocked()
|
|
{
|
|
if (!++m_prefetchGeneration)
|
|
++m_prefetchGeneration;
|
|
for (PrefetchBlock& block : m_prefetch)
|
|
if (block.state != PrefetchState::FILLING)
|
|
block.state = PrefetchState::EMPTY;
|
|
}
|
|
|
|
void SchedulePrefetchLocked()
|
|
{
|
|
if (!m_prefetchStarted || m_prefetchStop ||
|
|
m_offset >= m_entry.size)
|
|
return;
|
|
|
|
for (PrefetchBlock& block : m_prefetch)
|
|
{
|
|
if (block.state != PrefetchState::FILLING &&
|
|
block.generation != m_prefetchGeneration)
|
|
block.state = PrefetchState::EMPTY;
|
|
else if (block.state == PrefetchState::READY &&
|
|
block.offset + block.length <= m_offset)
|
|
block.state = PrefetchState::EMPTY;
|
|
}
|
|
|
|
uint64_t next = m_offset;
|
|
for (;;)
|
|
{
|
|
const PrefetchBlock * existing = nullptr;
|
|
for (const PrefetchBlock& block : m_prefetch)
|
|
if (Contains(block, next))
|
|
{
|
|
existing = █
|
|
break;
|
|
}
|
|
if (existing)
|
|
{
|
|
next = existing->offset + existing->length;
|
|
if (next >= m_entry.size)
|
|
break;
|
|
continue;
|
|
}
|
|
|
|
PrefetchBlock * empty = nullptr;
|
|
for (PrefetchBlock& block : m_prefetch)
|
|
if (block.state == PrefetchState::EMPTY)
|
|
{
|
|
empty = █
|
|
break;
|
|
}
|
|
if (!empty)
|
|
break;
|
|
|
|
empty->offset = next;
|
|
empty->length = static_cast<ULONG>((std::min<uint64_t>)(
|
|
FILE_READ_BYTES, m_entry.size - next));
|
|
empty->result = S_OK;
|
|
empty->generation = m_prefetchGeneration;
|
|
empty->state = PrefetchState::QUEUED;
|
|
next += empty->length;
|
|
m_prefetchWork.notify_one();
|
|
if (next >= m_entry.size)
|
|
break;
|
|
}
|
|
}
|
|
|
|
HRESULT StartPrefetchLocked()
|
|
{
|
|
if (m_prefetchStarted)
|
|
return S_OK;
|
|
try
|
|
{
|
|
const size_t blockBytes = static_cast<size_t>(
|
|
(std::min<uint64_t>)(FILE_READ_BYTES, m_entry.size));
|
|
for (PrefetchBlock& block : m_prefetch)
|
|
block.data.resize(blockBytes);
|
|
m_prefetchThread = std::thread(
|
|
&CClipboardFileStream::PrefetchThread, this);
|
|
}
|
|
catch (const std::bad_alloc&)
|
|
{
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
catch (const std::length_error&)
|
|
{
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
catch (const std::system_error&)
|
|
{
|
|
return E_FAIL;
|
|
}
|
|
m_prefetchStarted = true;
|
|
return S_OK;
|
|
}
|
|
|
|
void PrefetchThread()
|
|
{
|
|
std::unique_lock<std::mutex> lock(m_lock);
|
|
for (;;)
|
|
{
|
|
m_prefetchWork.wait(lock, [this]()
|
|
{
|
|
if (m_prefetchStop)
|
|
return true;
|
|
for (const PrefetchBlock& block : m_prefetch)
|
|
if (block.state == PrefetchState::QUEUED)
|
|
return true;
|
|
return false;
|
|
});
|
|
if (m_prefetchStop)
|
|
return;
|
|
|
|
PrefetchBlock * pending = nullptr;
|
|
for (PrefetchBlock& block : m_prefetch)
|
|
if (block.state == PrefetchState::QUEUED &&
|
|
(!pending || block.offset < pending->offset))
|
|
pending = █
|
|
if (!pending)
|
|
continue;
|
|
|
|
pending->state = PrefetchState::FILLING;
|
|
const uint64_t generation = pending->generation;
|
|
const uint64_t offset = pending->offset;
|
|
const ULONG length = pending->length;
|
|
uint8_t * const data = pending->data.data();
|
|
lock.unlock();
|
|
const HRESULT result = ReadProvider(offset, data, length);
|
|
lock.lock();
|
|
|
|
if (pending->state == PrefetchState::FILLING &&
|
|
pending->generation == generation)
|
|
{
|
|
if (m_prefetchStop || generation != m_prefetchGeneration)
|
|
pending->state = PrefetchState::EMPTY;
|
|
else
|
|
{
|
|
pending->result = result;
|
|
pending->state = PrefetchState::READY;
|
|
}
|
|
}
|
|
SchedulePrefetchLocked();
|
|
m_prefetchReady.notify_all();
|
|
}
|
|
}
|
|
|
|
void StopPrefetch()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_lock);
|
|
if (!m_prefetchStarted)
|
|
return;
|
|
m_prefetchStop = true;
|
|
InvalidatePrefetchLocked();
|
|
}
|
|
m_prefetchWork.notify_all();
|
|
m_prefetchReady.notify_all();
|
|
if (m_prefetchThread.joinable())
|
|
m_prefetchThread.join();
|
|
}
|
|
|
|
HRESULT ReadLocked(std::unique_lock<std::mutex>& lock, void * data,
|
|
ULONG length, ULONG * read)
|
|
{
|
|
if (read)
|
|
*read = 0;
|
|
if (!length)
|
|
return S_OK;
|
|
if (m_offset >= m_entry.size)
|
|
return S_FALSE;
|
|
|
|
const ULONG requested = static_cast<ULONG>((std::min<uint64_t>)(
|
|
length, m_entry.size - m_offset));
|
|
uint8_t * output = static_cast<uint8_t *>(data);
|
|
ULONG completed = 0;
|
|
|
|
const HRESULT started = StartPrefetchLocked();
|
|
if (FAILED(started))
|
|
return started;
|
|
|
|
while (completed < requested)
|
|
{
|
|
SchedulePrefetchLocked();
|
|
PrefetchBlock * ready = nullptr;
|
|
for (PrefetchBlock& block : m_prefetch)
|
|
if (block.state == PrefetchState::READY &&
|
|
Contains(block, m_offset))
|
|
{
|
|
ready = █
|
|
break;
|
|
}
|
|
if (!ready)
|
|
{
|
|
m_prefetchReady.wait(lock);
|
|
if (m_prefetchStop)
|
|
{
|
|
if (read)
|
|
*read = completed;
|
|
return STG_E_READFAULT;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (FAILED(ready->result))
|
|
{
|
|
const HRESULT result = ready->result;
|
|
InvalidatePrefetchLocked();
|
|
if (read)
|
|
*read = completed;
|
|
return result;
|
|
}
|
|
|
|
const size_t relative = static_cast<size_t>(
|
|
m_offset - ready->offset);
|
|
const ULONG amount = static_cast<ULONG>((std::min<size_t>)(
|
|
requested - completed, ready->length - relative));
|
|
memcpy(output + completed, ready->data.data() + relative, amount);
|
|
m_offset += amount;
|
|
completed += amount;
|
|
if (relative + amount == ready->length)
|
|
ready->state = PrefetchState::EMPTY;
|
|
}
|
|
|
|
SchedulePrefetchLocked();
|
|
if (read)
|
|
*read = completed;
|
|
return completed == length ? S_OK : S_FALSE;
|
|
}
|
|
|
|
public:
|
|
CClipboardFileStream(
|
|
const ClipboardRemoteFileEntry& entry,
|
|
std::shared_ptr<CClipboardFileDatasetLease> lease,
|
|
uint64_t offset = 0) :
|
|
m_entry(entry), m_lease(std::move(lease)), m_offset(offset)
|
|
{
|
|
}
|
|
|
|
~CClipboardFileStream()
|
|
{
|
|
StopPrefetch();
|
|
}
|
|
|
|
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<IStream *>(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 (!data && length)
|
|
{
|
|
if (read)
|
|
*read = 0;
|
|
return STG_E_INVALIDPOINTER;
|
|
}
|
|
std::lock_guard<std::mutex> operationLock(m_operationLock);
|
|
std::unique_lock<std::mutex> lock(m_lock);
|
|
return ReadLocked(lock, data, length, read);
|
|
}
|
|
|
|
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<std::mutex> operationLock(m_operationLock);
|
|
std::lock_guard<std::mutex> 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<uint64_t>(-(move.QuadPart + 1)) +
|
|
1U;
|
|
if (amount > base)
|
|
return STG_E_INVALIDFUNCTION;
|
|
next = base - amount;
|
|
}
|
|
else
|
|
{
|
|
const uint64_t amount = static_cast<uint64_t>(move.QuadPart);
|
|
if (amount > (std::numeric_limits<uint64_t>::max)() - base)
|
|
return STG_E_INVALIDFUNCTION;
|
|
next = base + amount;
|
|
}
|
|
m_offset = next;
|
|
InvalidatePrefetchLocked();
|
|
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<uint8_t> 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<ULONG>((std::min<uint64_t>)(
|
|
remaining, buffer.size()));
|
|
ULONG got = 0;
|
|
HRESULT result;
|
|
{
|
|
std::lock_guard<std::mutex> operationLock(m_operationLock);
|
|
std::unique_lock<std::mutex> lock(m_lock);
|
|
result = ReadLocked(lock, 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<LPOLESTR>(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<std::mutex> operationLock(m_operationLock);
|
|
std::lock_guard<std::mutex> 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<ULONG> m_refs { 1 };
|
|
std::vector<FORMATETC> m_formats;
|
|
size_t m_index = 0;
|
|
|
|
public:
|
|
CFormatEnumerator(std::vector<FORMATETC> 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<IEnumFORMATETC *>(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<size_t>)(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<ULONG> m_refs { 1 };
|
|
std::vector<ClipboardRemoteFileEntry> m_entries;
|
|
std::vector<FILEDESCRIPTORW> m_descriptors;
|
|
std::shared_ptr<CClipboardFileDatasetLease> 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<ClipboardRemoteFileEntry>&& entries,
|
|
std::vector<FILEDESCRIPTORW>&& 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<CClipboardFileDatasetLease> 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<IDataObject *>(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<UINT>::max)() ||
|
|
m_descriptors.size() > ((std::numeric_limits<SIZE_T>::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<FILEGROUPDESCRIPTORW *>(GlobalLock(memory));
|
|
if (!group)
|
|
{
|
|
GlobalFree(memory);
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
group->cItems = static_cast<UINT>(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<size_t>(format->lindex) >= m_entries.size())
|
|
return DV_E_LINDEX;
|
|
const ClipboardRemoteFileEntry& entry =
|
|
m_entries[static_cast<size_t>(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<DWORD *>(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<size_t>(format->lindex) < m_entries.size() &&
|
|
m_entries[static_cast<size_t>(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<FORMATETC> formats;
|
|
formats.reserve(3U);
|
|
formats.push_back({ static_cast<CLIPFORMAT>(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<CLIPFORMAT>(m_fileContents),
|
|
nullptr, DVASPECT_CONTENT, -1, TYMED_ISTREAM });
|
|
break;
|
|
}
|
|
formats.push_back({
|
|
static_cast<CLIPFORMAT>(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> CLocalClipboardFiles::Capture(
|
|
HDROP drop, KVMFRClipboardFileError& error)
|
|
{
|
|
error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID;
|
|
if (!drop)
|
|
{
|
|
SetLastError(ERROR_INVALID_HANDLE);
|
|
return nullptr;
|
|
}
|
|
std::vector<std::wstring> roots;
|
|
HANDLE userToken = nullptr;
|
|
std::shared_ptr<CLocalClipboardFiles> dataset;
|
|
try
|
|
{
|
|
DWORD tokenError = ERROR_SUCCESS;
|
|
if (!CaptureUserToken(userToken, error, tokenError))
|
|
return nullptr;
|
|
CLocalClipboardFiles * raw = new CLocalClipboardFiles(userToken);
|
|
userToken = nullptr;
|
|
dataset = std::shared_ptr<CLocalClipboardFiles>(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<wchar_t> path(static_cast<size_t>(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(impersonation.Error());
|
|
return nullptr;
|
|
}
|
|
for (const std::wstring& root : roots)
|
|
if (!dataset->AddRoot(root, error))
|
|
return nullptr;
|
|
if (!impersonation.Finish())
|
|
{
|
|
error = TokenError(impersonation.Error());
|
|
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<size_t>(root - 1U)].name,
|
|
node.name))
|
|
{
|
|
error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID;
|
|
return false;
|
|
}
|
|
|
|
node.id = static_cast<uint64_t>(m_nodes.size()) + 1U;
|
|
m_nodes.emplace_back(std::move(node));
|
|
m_roots.push_back(static_cast<uint64_t>(m_nodes.size()));
|
|
return true;
|
|
}
|
|
|
|
bool CLocalClipboardFiles::RefreshRoots(KVMFRClipboardFileError& error)
|
|
{
|
|
std::vector<Node> refreshed;
|
|
try
|
|
{
|
|
refreshed.reserve(m_roots.size());
|
|
for (uint64_t id : m_roots)
|
|
{
|
|
const Node& source = m_nodes[static_cast<size_t>(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<size_t>(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<uint64_t>(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<Node> 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<uint64_t>& ids = m_nodes[index].children;
|
|
ids.reserve(children.size());
|
|
for (Node& child : children)
|
|
{
|
|
child.id = static_cast<uint64_t>(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<uint8_t>& data,
|
|
KVMFRClipboardFileError& error)
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_lock);
|
|
error = KVMFR_CLIPBOARD_FILE_ERROR_NONE;
|
|
const std::vector<uint64_t> * children = nullptr;
|
|
if (node == KVMFR_CLIPBOARD_FILE_ROOT_NODE)
|
|
{
|
|
if (!m_published)
|
|
{
|
|
CThreadImpersonation impersonation(m_userToken);
|
|
if (!impersonation.Active())
|
|
{
|
|
error = TokenError(impersonation.Error());
|
|
return false;
|
|
}
|
|
if (!RefreshRoots(error))
|
|
return false;
|
|
if (!impersonation.Finish())
|
|
{
|
|
error = TokenError(impersonation.Error());
|
|
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<size_t>(node - 1U);
|
|
CThreadImpersonation impersonation(m_userToken);
|
|
if (!impersonation.Active())
|
|
{
|
|
error = TokenError(impersonation.Error());
|
|
return false;
|
|
}
|
|
if (!LoadChildren(index, error))
|
|
return false;
|
|
if (!impersonation.Finish())
|
|
{
|
|
error = TokenError(impersonation.Error());
|
|
return false;
|
|
}
|
|
children = &m_nodes[index].children;
|
|
}
|
|
|
|
data.clear();
|
|
try
|
|
{
|
|
for (uint64_t id : *children)
|
|
{
|
|
const Node& child = m_nodes[static_cast<size_t>(id - 1U)];
|
|
std::vector<uint8_t> name;
|
|
if (!ToUTF8(child.name, name))
|
|
{
|
|
error = FileError(GetLastError());
|
|
return false;
|
|
}
|
|
if (name.size() > (std::numeric_limits<uint32_t>::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<size_t>::max)() ||
|
|
data.size() > (std::numeric_limits<size_t>::max)() -
|
|
static_cast<size_t>(recordBytes))
|
|
{
|
|
error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY;
|
|
return false;
|
|
}
|
|
const size_t offset = data.size();
|
|
data.resize(offset + static_cast<size_t>(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<uint32_t>(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<uint8_t>& data,
|
|
KVMFRClipboardFileError& error)
|
|
{
|
|
error = KVMFR_CLIPBOARD_FILE_ERROR_NONE;
|
|
std::wstring path;
|
|
Node expected;
|
|
{
|
|
std::lock_guard<std::mutex> 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<size_t>(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<uint64_t>(
|
|
(std::numeric_limits<LONGLONG>::max)()) ||
|
|
offset > (std::numeric_limits<uint64_t>::max)() - length)
|
|
{
|
|
error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID;
|
|
return false;
|
|
}
|
|
|
|
CThreadImpersonation impersonation(m_userToken);
|
|
if (!impersonation.Active())
|
|
{
|
|
error = TokenError(impersonation.Error());
|
|
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<std::mutex> 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<size_t>(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<uint64_t>(
|
|
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<uint32_t>((std::min<uint64_t>)(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(impersonation.Error());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
HRESULT ParseClipboardFileList(uint64_t parent, const uint8_t * data,
|
|
size_t length, std::vector<ClipboardRemoteFileEntry>& 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<size_t>(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<size_t>(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<ClipboardRemoteFileEntry>&& entries,
|
|
std::shared_ptr<IRemoteClipboardFileProvider> 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<size_t>((std::numeric_limits<LONG>::max)()) ||
|
|
entries.size() >
|
|
static_cast<size_t>((std::numeric_limits<UINT>::max)()))
|
|
return HRESULT_FROM_WIN32(ERROR_INVALID_DATA);
|
|
|
|
std::unordered_map<uint64_t, size_t> byID;
|
|
std::map<uint64_t, std::set<std::wstring, ComponentLess>> 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<std::wstring> paths(entries.size());
|
|
std::vector<uint8_t> state(entries.size(), 0);
|
|
std::vector<size_t> chain;
|
|
chain.reserve((std::min<size_t>)(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<FILEDESCRIPTORW> 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<DWORD>(entry.size >> 32);
|
|
descriptor.nFileSizeLow = static_cast<DWORD>(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<CClipboardFileDatasetLease> lease =
|
|
std::make_shared<CClipboardFileDatasetLease>(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;
|
|
}
|
|
}
|