From f215e02bf85f68d3a6106c2a1f4f7f063f819064 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 11 Apr 2024 10:17:27 +0200 Subject: Adding upstream version 7.0.14-dfsg. Signed-off-by: Daniel Baumann --- src/VBox/GuestHost/DragAndDrop/DnDDroppedFiles.cpp | 450 +++ src/VBox/GuestHost/DragAndDrop/DnDMIME.cpp | 63 + src/VBox/GuestHost/DragAndDrop/DnDPath.cpp | 212 ++ src/VBox/GuestHost/DragAndDrop/DnDTransferList.cpp | 1295 ++++++++ .../GuestHost/DragAndDrop/DnDTransferObject.cpp | 706 +++++ src/VBox/GuestHost/DragAndDrop/DnDUtils.cpp | 177 ++ src/VBox/GuestHost/DragAndDrop/Makefile.kmk | 68 + .../GuestHost/DragAndDrop/testcase/Makefile.kmk | 81 + .../GuestHost/DragAndDrop/testcase/tstDnDPath.cpp | 100 + .../DragAndDrop/testcase/tstDnDTransferList.cpp | 206 ++ .../DragAndDrop/testcase/tstDnDTransferObject.cpp | 128 + .../testcase/tstDragAndDropQt/Makefile.kmk | 53 + .../testcase/tstDragAndDropQt/tstDragAndDropQt.cpp | 67 + src/VBox/GuestHost/HGSMI/.scm-settings | 31 + src/VBox/GuestHost/HGSMI/HGSMICommon.cpp | 450 +++ src/VBox/GuestHost/HGSMI/HGSMIMemAlloc.cpp | 684 +++++ src/VBox/GuestHost/HGSMI/Makefile.kmk | 59 + src/VBox/GuestHost/Makefile.kmk | 45 + .../ClipboardDataObjectImpl-win.cpp | 978 ++++++ .../ClipboardEnumFormatEtcImpl-win.cpp | 205 ++ .../GuestHost/SharedClipboard/ClipboardMIME.cpp | 53 + .../GuestHost/SharedClipboard/ClipboardPath.cpp | 92 + .../SharedClipboard/ClipboardStreamImpl-win.cpp | 382 +++ src/VBox/GuestHost/SharedClipboard/Makefile.kmk | 36 + .../GuestHost/SharedClipboard/clipboard-common.cpp | 1226 ++++++++ .../SharedClipboard/clipboard-transfers-http.cpp | 862 ++++++ .../SharedClipboard/clipboard-transfers.cpp | 3132 ++++++++++++++++++++ .../GuestHost/SharedClipboard/clipboard-win.cpp | 1322 +++++++++ .../GuestHost/SharedClipboard/clipboard-x11.cpp | 2428 +++++++++++++++ .../SharedClipboard/testcase/Makefile.kmk | 76 + .../testcase/tstClipboardGH-X11.cpp | 941 ++++++ .../testcase/tstClipboardGH-X11Smoke.cpp | 103 + .../testcase/tstClipboardQt/Makefile.kmk | 52 + .../testcase/tstClipboardQt/tstClipboardQt.cpp | 47 + 34 files changed, 16810 insertions(+) create mode 100644 src/VBox/GuestHost/DragAndDrop/DnDDroppedFiles.cpp create mode 100644 src/VBox/GuestHost/DragAndDrop/DnDMIME.cpp create mode 100644 src/VBox/GuestHost/DragAndDrop/DnDPath.cpp create mode 100644 src/VBox/GuestHost/DragAndDrop/DnDTransferList.cpp create mode 100644 src/VBox/GuestHost/DragAndDrop/DnDTransferObject.cpp create mode 100644 src/VBox/GuestHost/DragAndDrop/DnDUtils.cpp create mode 100644 src/VBox/GuestHost/DragAndDrop/Makefile.kmk create mode 100644 src/VBox/GuestHost/DragAndDrop/testcase/Makefile.kmk create mode 100644 src/VBox/GuestHost/DragAndDrop/testcase/tstDnDPath.cpp create mode 100644 src/VBox/GuestHost/DragAndDrop/testcase/tstDnDTransferList.cpp create mode 100644 src/VBox/GuestHost/DragAndDrop/testcase/tstDnDTransferObject.cpp create mode 100644 src/VBox/GuestHost/DragAndDrop/testcase/tstDragAndDropQt/Makefile.kmk create mode 100644 src/VBox/GuestHost/DragAndDrop/testcase/tstDragAndDropQt/tstDragAndDropQt.cpp create mode 100644 src/VBox/GuestHost/HGSMI/.scm-settings create mode 100644 src/VBox/GuestHost/HGSMI/HGSMICommon.cpp create mode 100644 src/VBox/GuestHost/HGSMI/HGSMIMemAlloc.cpp create mode 100644 src/VBox/GuestHost/HGSMI/Makefile.kmk create mode 100644 src/VBox/GuestHost/Makefile.kmk create mode 100644 src/VBox/GuestHost/SharedClipboard/ClipboardDataObjectImpl-win.cpp create mode 100644 src/VBox/GuestHost/SharedClipboard/ClipboardEnumFormatEtcImpl-win.cpp create mode 100644 src/VBox/GuestHost/SharedClipboard/ClipboardMIME.cpp create mode 100644 src/VBox/GuestHost/SharedClipboard/ClipboardPath.cpp create mode 100644 src/VBox/GuestHost/SharedClipboard/ClipboardStreamImpl-win.cpp create mode 100644 src/VBox/GuestHost/SharedClipboard/Makefile.kmk create mode 100644 src/VBox/GuestHost/SharedClipboard/clipboard-common.cpp create mode 100644 src/VBox/GuestHost/SharedClipboard/clipboard-transfers-http.cpp create mode 100644 src/VBox/GuestHost/SharedClipboard/clipboard-transfers.cpp create mode 100644 src/VBox/GuestHost/SharedClipboard/clipboard-win.cpp create mode 100644 src/VBox/GuestHost/SharedClipboard/clipboard-x11.cpp create mode 100644 src/VBox/GuestHost/SharedClipboard/testcase/Makefile.kmk create mode 100644 src/VBox/GuestHost/SharedClipboard/testcase/tstClipboardGH-X11.cpp create mode 100644 src/VBox/GuestHost/SharedClipboard/testcase/tstClipboardGH-X11Smoke.cpp create mode 100644 src/VBox/GuestHost/SharedClipboard/testcase/tstClipboardQt/Makefile.kmk create mode 100644 src/VBox/GuestHost/SharedClipboard/testcase/tstClipboardQt/tstClipboardQt.cpp (limited to 'src/VBox/GuestHost') diff --git a/src/VBox/GuestHost/DragAndDrop/DnDDroppedFiles.cpp b/src/VBox/GuestHost/DragAndDrop/DnDDroppedFiles.cpp new file mode 100644 index 00000000..5dafc3bf --- /dev/null +++ b/src/VBox/GuestHost/DragAndDrop/DnDDroppedFiles.cpp @@ -0,0 +1,450 @@ +/* $Id: DnDDroppedFiles.cpp $ */ +/** @file + * DnD - Directory handling. + */ + +/* + * Copyright (C) 2014-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_GUEST_DND +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + + +/********************************************************************************************************************************* +* Prototypes * +*********************************************************************************************************************************/ +static int dndDroppedFilesCloseInternal(PDNDDROPPEDFILES pDF); + + +/** + * Initializes a DnD Dropped Files struct, internal version. + * + * @returns VBox status code. + * @param pDF DnD Dropped Files to initialize. + */ +static int dndDroppedFilesInitInternal(PDNDDROPPEDFILES pDF) +{ + pDF->m_fOpen = 0; + pDF->m_hDir = NIL_RTDIR; + pDF->pszPathAbs = NULL; + + RTListInit(&pDF->m_lstDirs); + RTListInit(&pDF->m_lstFiles); + + return VINF_SUCCESS; +} + +/** + * Initializes a DnD Dropped Files struct, extended version. + * + * @returns VBox status code. + * @param pDF DnD Dropped Files to initialize. + * @param fFlags Dropped Files flags to use for initialization. + */ +int DnDDroppedFilesInitEx(PDNDDROPPEDFILES pDF, + const char *pszPath, DNDURIDROPPEDFILEFLAGS fFlags /* = DNDURIDROPPEDFILE_FLAGS_NONE */) +{ + int rc = dndDroppedFilesInitInternal(pDF); + if (RT_FAILURE(rc)) + return rc; + + return DnDDroppedFilesOpenEx(pDF, pszPath, fFlags); +} + +/** + * Initializes a DnD Dropped Files struct. + * + * @returns VBox status code. + * @param pDF DnD Dropped Files to initialize. + */ +int DnDDroppedFilesInit(PDNDDROPPEDFILES pDF) +{ + return dndDroppedFilesInitInternal(pDF); +} + +/** + * Destroys a DnD Dropped Files struct. + * + * Note: This does *not* (physically) delete any added content. + * Make sure to call DnDDroppedFilesReset() then. + * + * @param pDF DnD Dropped Files to destroy. + */ +void DnDDroppedFilesDestroy(PDNDDROPPEDFILES pDF) +{ + /* Only make sure to not leak any handles and stuff, don't delete any + * directories / files here. */ + dndDroppedFilesCloseInternal(pDF); + + RTStrFree(pDF->pszPathAbs); + pDF->pszPathAbs = NULL; +} + +/** + * Adds a file reference to a Dropped Files directory. + * + * @returns VBox status code. + * @param pDF DnD Dropped Files to add file to. + * @param pszFile Path of file entry to add. + */ +int DnDDroppedFilesAddFile(PDNDDROPPEDFILES pDF, const char *pszFile) +{ + AssertPtrReturn(pszFile, VERR_INVALID_POINTER); + + PDNDDROPPEDFILESENTRY pEntry = (PDNDDROPPEDFILESENTRY)RTMemAlloc(sizeof(DNDDROPPEDFILESENTRY)); + if (!pEntry) + return VERR_NO_MEMORY; + + pEntry->pszPath = RTStrDup(pszFile); + if (pEntry->pszPath) + { + RTListAppend(&pDF->m_lstFiles, &pEntry->Node); + return VINF_SUCCESS; + } + + RTMemFree(pEntry); + return VERR_NO_MEMORY; +} + +/** + * Adds a directory reference to a Dropped Files directory. + * + * Note: This does *not* (recursively) add sub entries. + * + * @returns VBox status code. + * @param pDF DnD Dropped Files to add directory to. + * @param pszDir Path of directory entry to add. + */ +int DnDDroppedFilesAddDir(PDNDDROPPEDFILES pDF, const char *pszDir) +{ + AssertPtrReturn(pszDir, VERR_INVALID_POINTER); + + PDNDDROPPEDFILESENTRY pEntry = (PDNDDROPPEDFILESENTRY)RTMemAlloc(sizeof(DNDDROPPEDFILESENTRY)); + if (!pEntry) + return VERR_NO_MEMORY; + + pEntry->pszPath = RTStrDup(pszDir); + if (pEntry->pszPath) + { + RTListAppend(&pDF->m_lstDirs, &pEntry->Node); + return VINF_SUCCESS; + } + + RTMemFree(pEntry); + return VERR_NO_MEMORY; +} + +/** + * Closes the dropped files directory handle, internal version. + * + * @returns VBox status code. + * @param pDF DnD Dropped Files to close. + */ +static int dndDroppedFilesCloseInternal(PDNDDROPPEDFILES pDF) +{ + int rc; + if (pDF->m_hDir != NULL) + { + rc = RTDirClose(pDF->m_hDir); + if (RT_SUCCESS(rc)) + pDF->m_hDir = NULL; + } + else + rc = VINF_SUCCESS; + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Closes the dropped files directory handle. + * + * @returns VBox status code. + * @param pDF DnD Dropped Files to close. + */ +int DnDDroppedFilesClose(PDNDDROPPEDFILES pDF) +{ + return dndDroppedFilesCloseInternal(pDF); +} + +/** + * Returns the absolute path of the dropped files directory. + * + * @returns Pointer to absolute path of the dropped files directory. + * @param pDF DnD Dropped Files return absolute path of the dropped files directory for. + */ +const char *DnDDroppedFilesGetDirAbs(PDNDDROPPEDFILES pDF) +{ + return pDF->pszPathAbs; +} + +/** + * Returns whether the dropped files directory has been opened or not. + * + * @returns \c true if open, \c false if not. + * @param pDF DnD Dropped Files to return open status for. + */ +bool DnDDroppedFilesIsOpen(PDNDDROPPEDFILES pDF) +{ + return (pDF->m_hDir != NULL); +} + +/** + * Opens (creates) the dropped files directory. + * + * @returns VBox status code. + * @param pDF DnD Dropped Files to open. + * @param pszPath Absolute path where to create the dropped files directory. + * @param fFlags Dropped files flags to use for this directory. + */ +int DnDDroppedFilesOpenEx(PDNDDROPPEDFILES pDF, + const char *pszPath, DNDURIDROPPEDFILEFLAGS fFlags /* = DNDURIDROPPEDFILE_FLAGS_NONE */) +{ + AssertPtrReturn(pszPath, VERR_INVALID_POINTER); + AssertReturn(fFlags == 0, VERR_INVALID_PARAMETER); /* Flags not supported yet. */ + + int rc; + + do + { + char szDropDir[RTPATH_MAX]; + RTStrPrintf(szDropDir, sizeof(szDropDir), "%s", pszPath); + + /** @todo On Windows we also could use the registry to override + * this path, on Posix a dotfile and/or a guest property + * can be used. */ + + /* Append our base drop directory. */ + rc = RTPathAppend(szDropDir, sizeof(szDropDir), "VirtualBox Dropped Files"); /** @todo Make this tag configurable? */ + if (RT_FAILURE(rc)) + break; + + /* Create it when necessary. */ + if (!RTDirExists(szDropDir)) + { + rc = RTDirCreateFullPath(szDropDir, RTFS_UNIX_IRWXU); + if (RT_FAILURE(rc)) + break; + } + + /* The actually drop directory consist of the current time stamp and a + * unique number when necessary. */ + char szTime[64]; + RTTIMESPEC time; + if (!RTTimeSpecToString(RTTimeNow(&time), szTime, sizeof(szTime))) + { + rc = VERR_BUFFER_OVERFLOW; + break; + } + + rc = DnDPathSanitizeFileName(szTime, sizeof(szTime)); + if (RT_FAILURE(rc)) + break; + + rc = RTPathAppend(szDropDir, sizeof(szDropDir), szTime); + if (RT_FAILURE(rc)) + break; + + /* Create it (only accessible by the current user) */ + rc = RTDirCreateUniqueNumbered(szDropDir, sizeof(szDropDir), RTFS_UNIX_IRWXU, 3, '-'); + if (RT_SUCCESS(rc)) + { + RTDIR hDir; + rc = RTDirOpen(&hDir, szDropDir); + if (RT_SUCCESS(rc)) + { + pDF->pszPathAbs = RTStrDup(szDropDir); + AssertPtrBreakStmt(pDF->pszPathAbs, rc = VERR_NO_MEMORY); + pDF->m_hDir = hDir; + pDF->m_fOpen = fFlags; + } + } + + } while (0); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Opens (creates) the dropped files directory in the system's temp directory. + * + * @returns VBox status code. + * @param pDF DnD Dropped Files to open. + * @param fFlags Dropped files flags to use for this directory. + */ +int DnDDroppedFilesOpenTemp(PDNDDROPPEDFILES pDF, DNDURIDROPPEDFILEFLAGS fFlags) +{ + AssertReturn(fFlags == 0, VERR_INVALID_PARAMETER); /* Flags not supported yet. */ + + /* + * Get the user's temp directory. Don't use the user's root directory (or + * something inside it) because we don't know for how long/if the data will + * be kept after the guest OS used it. + */ + char szTemp[RTPATH_MAX]; + int rc = RTPathTemp(szTemp, sizeof(szTemp)); + if (RT_SUCCESS(rc)) + rc = DnDDroppedFilesOpenEx(pDF, szTemp, fFlags); + + return rc; +} + +/** + * Free's an internal DnD Dropped Files entry. + * + * @param pEntry Pointer to entry to free. The pointer will be invalid after calling. + */ +static void dndDroppedFilesEntryFree(PDNDDROPPEDFILESENTRY pEntry) +{ + if (!pEntry) + return; + RTStrFree(pEntry->pszPath); + RTListNodeRemove(&pEntry->Node); + RTMemFree(pEntry); +} + +/** + * Resets an internal DnD Dropped Files list. + * + * @param pListAnchor Pointer to list (anchor) to reset. + */ +static void dndDroppedFilesResetList(PRTLISTANCHOR pListAnchor) +{ + PDNDDROPPEDFILESENTRY pEntryCur, pEntryNext; + RTListForEachSafe(pListAnchor, pEntryCur, pEntryNext, DNDDROPPEDFILESENTRY, Node) + dndDroppedFilesEntryFree(pEntryCur); + Assert(RTListIsEmpty(pListAnchor)); +} + +/** + * Resets a droppped files directory. + * + * @returns VBox status code. + * @param pDF DnD Dropped Files to reset. + * @param fDelete Whether to physically delete the directory and its content + * or just clear the internal references. + */ +int DnDDroppedFilesReset(PDNDDROPPEDFILES pDF, bool fDelete) +{ + int rc = dndDroppedFilesCloseInternal(pDF); + if (RT_SUCCESS(rc)) + { + if (fDelete) + { + rc = DnDDroppedFilesRollback(pDF); + } + else + { + dndDroppedFilesResetList(&pDF->m_lstDirs); + dndDroppedFilesResetList(&pDF->m_lstFiles); + } + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Re-opens a droppes files directory. + * + * @returns VBox status code, or VERR_NOT_FOUND if the dropped files directory has not been opened before. + * @param pDF DnD Dropped Files to re-open. + */ +int DnDDroppedFilesReopen(PDNDDROPPEDFILES pDF) +{ + if (!pDF->pszPathAbs) + return VERR_NOT_FOUND; + + return DnDDroppedFilesOpenEx(pDF, pDF->pszPathAbs, pDF->m_fOpen); +} + +/** + * Performs a rollback of a dropped files directory. + * This cleans the directory by physically deleting all files / directories which have been added before. + * + * @returns VBox status code. + * @param pDF DnD Dropped Files to roll back. + */ +int DnDDroppedFilesRollback(PDNDDROPPEDFILES pDF) +{ + if (!pDF->pszPathAbs) + return VINF_SUCCESS; + + int rc = VINF_SUCCESS; + + /* Rollback by removing any stuff created. + * Note: Only remove empty directories, never ever delete + * anything recursive here! Steam (tm) knows best ... :-) */ + int rc2; + PDNDDROPPEDFILESENTRY pEntryCur, pEntryNext; + RTListForEachSafe(&pDF->m_lstFiles, pEntryCur, pEntryNext, DNDDROPPEDFILESENTRY, Node) + { + rc2 = RTFileDelete(pEntryCur->pszPath); + if (RT_SUCCESS(rc2)) + dndDroppedFilesEntryFree(pEntryCur); + else if (RT_SUCCESS(rc)) + rc = rc2; + /* Keep going. */ + } + + RTListForEachSafe(&pDF->m_lstDirs, pEntryCur, pEntryNext, DNDDROPPEDFILESENTRY, Node) + { + rc2 = RTDirRemove(pEntryCur->pszPath); + if (RT_SUCCESS(rc2)) + dndDroppedFilesEntryFree(pEntryCur); + else if (RT_SUCCESS(rc)) + rc = rc2; + /* Keep going. */ + } + + if (RT_SUCCESS(rc)) + { + rc2 = dndDroppedFilesCloseInternal(pDF); + if (RT_SUCCESS(rc2)) + { + /* Try to remove the empty root dropped files directory as well. + * Might return VERR_DIR_NOT_EMPTY or similar. */ + rc2 = RTDirRemove(pDF->pszPathAbs); + } + if (RT_SUCCESS(rc)) + rc = rc2; + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + diff --git a/src/VBox/GuestHost/DragAndDrop/DnDMIME.cpp b/src/VBox/GuestHost/DragAndDrop/DnDMIME.cpp new file mode 100644 index 00000000..cbb4a077 --- /dev/null +++ b/src/VBox/GuestHost/DragAndDrop/DnDMIME.cpp @@ -0,0 +1,63 @@ +/* $Id: DnDMIME.cpp $ */ +/** @file + * DnD - Path list class. + */ + +/* + * Copyright (C) 2014-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_GUEST_DND +#include + +#include + + +/** + * Returns whether a given MIME format contains file URLs we can work with or not. + * + * @returns \c true if the format contains file URLs, \c false if not. + * @param pcszFormat MIME format to check. + * @param cchFormatMax Maximum characters of MIME format to check. + */ +bool DnDMIMEHasFileURLs(const char *pcszFormat, size_t cchFormatMax) +{ + /** @todo "text/uri" also an official variant? */ + return ( RTStrNICmp(pcszFormat, "text/uri-list", cchFormatMax) == 0 + || RTStrNICmp(pcszFormat, "x-special/gnome-icon-list", cchFormatMax) == 0); +} + +/** + * Returns whether a given MIME format needs an own "Dropped Files" directory or not. + * + * @returns \c true if the format needs an own "Dropped Files" directory, \c false if not. + * @param pcszFormat MIME format to check. + * @param cchFormatMax Maximum characters of MIME format to check. + */ +bool DnDMIMENeedsDropDir(const char *pcszFormat, size_t cchFormatMax) +{ + return DnDMIMEHasFileURLs(pcszFormat, cchFormatMax); +} + diff --git a/src/VBox/GuestHost/DragAndDrop/DnDPath.cpp b/src/VBox/GuestHost/DragAndDrop/DnDPath.cpp new file mode 100644 index 00000000..e4ac219e --- /dev/null +++ b/src/VBox/GuestHost/DragAndDrop/DnDPath.cpp @@ -0,0 +1,212 @@ +/* $Id: DnDPath.cpp $ */ +/** @file + * DnD - Path handling. + */ + +/* + * Copyright (C) 2014-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_GUEST_DND +#include + +#include +#include +#include +#include +#include +#include + + +/** + * Sanitizes the file name portion of a path so that unsupported characters will be replaced by an underscore ("_"). + * + * @return IPRT status code. + * @param pszFileName File name to sanitize. + * @param cbFileName Size (in bytes) of file name to sanitize. + */ +int DnDPathSanitizeFileName(char *pszFileName, size_t cbFileName) +{ + if (!pszFileName) /* No path given? Bail out early. */ + return VINF_SUCCESS; + + AssertReturn(cbFileName, VERR_INVALID_PARAMETER); + + int rc = VINF_SUCCESS; +#ifdef RT_OS_WINDOWS + RT_NOREF1(cbFileName); + /* Replace out characters not allowed on Windows platforms, put in by RTTimeSpecToString(). */ + /** @todo Use something like RTPathSanitize() if available later some time. */ + static const RTUNICP s_uszValidRangePairs[] = + { + ' ', ' ', + '(', ')', + '-', '.', + '0', '9', + 'A', 'Z', + 'a', 'z', + '_', '_', + 0xa0, 0xd7af, + '\0' + }; + + ssize_t cReplaced = RTStrPurgeComplementSet(pszFileName, s_uszValidRangePairs, '_' /* chReplacement */); + if (cReplaced < 0) + rc = VERR_INVALID_UTF8_ENCODING; +#else + RT_NOREF2(pszFileName, cbFileName); +#endif + return rc; +} + +/** + * Validates whether a given path matches our set of rules or not. + * + * Rules: + * - An empty path is allowed. + * - Dot components ("." or "..") are forbidden. + * - If \a fMustExist is \c true, the path either has to be a file or a directory and must exist. + * - Symbolic links are forbidden. + * + * @returns VBox status code. + * @param pcszPath Path to validate. + * @param fMustExist Whether the path to validate also must exist. + * @sa shClTransferValidatePath(). + */ +int DnDPathValidate(const char *pcszPath, bool fMustExist) +{ + if (!pcszPath) + return VERR_INVALID_POINTER; + + int rc = VINF_SUCCESS; + + if ( RT_SUCCESS(rc) + && !RTStrIsValidEncoding(pcszPath)) + { + rc = VERR_INVALID_UTF8_ENCODING; + } + + if ( RT_SUCCESS(rc) + && RTStrStr(pcszPath, "..")) + { + rc = VERR_INVALID_PARAMETER; + } + + if ( RT_SUCCESS(rc) + && fMustExist) + { + RTFSOBJINFO objInfo; + rc = RTPathQueryInfo(pcszPath, &objInfo, RTFSOBJATTRADD_NOTHING); + if (RT_SUCCESS(rc)) + { + if (RTFS_IS_DIRECTORY(objInfo.Attr.fMode)) + { + if (!RTDirExists(pcszPath)) /* Path must exist. */ + rc = VERR_PATH_NOT_FOUND; + } + else if (RTFS_IS_FILE(objInfo.Attr.fMode)) + { + if (!RTFileExists(pcszPath)) /* File must exist. */ + rc = VERR_FILE_NOT_FOUND; + } + else /* Everything else (e.g. symbolic links) are not supported. */ + rc = VERR_NOT_SUPPORTED; + } + } + + return rc; +} + +/** + * Converts a DnD path. + * + * @returns VBox status code. + * @param pszPath Path to convert. + * @param cbPath Size (in bytes) of path to convert. + * @param fFlags Conversion flags of type DNDPATHCONVERT_FLAGS_. + */ +int DnDPathConvert(char *pszPath, size_t cbPath, DNDPATHCONVERTFLAGS fFlags) +{ + RT_NOREF(cbPath); + AssertReturn(!(fFlags & ~DNDPATHCONVERT_FLAGS_VALID_MASK), VERR_INVALID_FLAGS); + + if (fFlags & DNDPATHCONVERT_FLAGS_TO_DOS) + RTPathChangeToDosSlashes(pszPath, true /* fForce */); + else + RTPathChangeToUnixSlashes(pszPath, true /* fForce */); + + return VINF_SUCCESS; +} + +/** + * Rebases an absolute path from an old path base to a new path base. + * Note: Does *not* do any path conversion. + * + * @return IPRT status code. + * @param pcszPath Path to rebase. + * @param strBaseOld Old base path to rebase from. Optional and can be NULL. + * @param strBaseNew New base path to rebase to. + * @param ppszPath Where to store the allocated rebased path on success. Needs to be free'd with RTStrFree(). + */ +int DnDPathRebase(const char *pcszPath, const char *pcszBaseOld, const char *pcszBaseNew, + char **ppszPath) +{ + AssertPtrReturn(pcszPath, VERR_INVALID_POINTER); + AssertPtrReturn(pcszBaseOld, VERR_INVALID_POINTER); + AssertPtrReturn(pcszBaseNew, VERR_INVALID_POINTER); + AssertPtrReturn(ppszPath, VERR_INVALID_POINTER); + + char szPath[RTPATH_MAX]; + + /* Do we need to see if the given path is part of the old base? */ + size_t idxBase; + if ( pcszBaseOld + && RTPathStartsWith(pcszPath, pcszBaseOld)) + { + idxBase = strlen(pcszBaseOld); + } + else + idxBase = 0; + + int rc = RTStrCopy(szPath, sizeof(szPath), pcszBaseNew); + if (RT_SUCCESS(rc)) + { + rc = RTPathAppend(szPath, sizeof(szPath), &pcszPath[idxBase]); + if (RT_SUCCESS(rc)) + rc = DnDPathValidate(szPath, false /* fMustExist */); + } + + if (RT_SUCCESS(rc)) + { + char *pszPath = RTStrDup(szPath); + if (pszPath) + *ppszPath = pszPath; + else + rc = VERR_NO_MEMORY; + } + + return rc; +} + diff --git a/src/VBox/GuestHost/DragAndDrop/DnDTransferList.cpp b/src/VBox/GuestHost/DragAndDrop/DnDTransferList.cpp new file mode 100644 index 00000000..de7893b8 --- /dev/null +++ b/src/VBox/GuestHost/DragAndDrop/DnDTransferList.cpp @@ -0,0 +1,1295 @@ +/* $Id: DnDTransferList.cpp $ */ +/** @file + * DnD - transfer list implemenation. + */ + +/* + * Copyright (C) 2014-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +/** + * This implementation is taylored to keeping track of a single DnD transfer by maintaining two separate entities, + * namely a list of root entries and a list of (recursive file system) transfer ojects to actually transfer. + * + * The list of root entries is sent to the target (guest/host) beforehand so that the OS has a data for the + * actual drag'n drop operation to work with. This also contains required header data like total number of + * objects or total bytes being received. + * + * The list of transfer objects only is needed in order to sending data from the source to the target. + * Currently there is no particular ordering implemented for the transfer object list; it depends on IPRT's RTDirRead(). + * + * The target must not know anything about the actual (absolute) path the root entries are coming from + * due to security reasons. Those root entries then can be re-based on the target to desired location there. + * + * All data handling internally is done in the so-called "transport" format, that is, non-URI (regular) paths + * with the "/" as path separator. From/to URI conversion is provided for convenience only. + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_GUEST_DND +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + + +/********************************************************************************************************************************* +* Prototypes * +*********************************************************************************************************************************/ +static int dndTransferListSetRootPath(PDNDTRANSFERLIST pList, const char *pcszRootPathAbs); + +static int dndTransferListRootEntryAdd(PDNDTRANSFERLIST pList, const char *pcszRoot); +static void dndTransferListRootEntryFree(PDNDTRANSFERLIST pList, PDNDTRANSFERLISTROOT pRootObj); + +static int dndTransferListObjAdd(PDNDTRANSFERLIST pList, const char *pcszSrcAbs, RTFMODE fMode, DNDTRANSFERLISTFLAGS fFlags); +static void dndTransferListObjFree(PDNDTRANSFERLIST pList, PDNDTRANSFEROBJECT pLstObj); + + +/** The size of the directory entry buffer we're using. */ +#define DNDTRANSFERLIST_DIRENTRY_BUF_SIZE (sizeof(RTDIRENTRYEX) + RTPATH_MAX) + + +/** + * Initializes a transfer list, internal version. + * + * @returns VBox status code. + * @param pList Transfer list to initialize. + * @param pcszRootPathAbs Absolute root path to use for this list. Optional and can be NULL. + */ +static int dndTransferListInitInternal(PDNDTRANSFERLIST pList, const char *pcszRootPathAbs) +{ + AssertPtrReturn(pList, VERR_INVALID_POINTER); + /* pcszRootPathAbs is optional. */ + + if (pList->pszPathRootAbs) /* Already initialized? */ + return VERR_WRONG_ORDER; + + pList->pszPathRootAbs = NULL; + + RTListInit(&pList->lstRoot); + pList->cRoots = 0; + + RTListInit(&pList->lstObj); + pList->cObj = 0; + pList->cbObjTotal = 0; + + if (pcszRootPathAbs) + return dndTransferListSetRootPath(pList, pcszRootPathAbs); + + return VINF_SUCCESS; +} + +/** + * Initializes a transfer list, extended version. + * + * @returns VBox status code. + * @param pList Transfer list to initialize. + * @param pcszRootPathAbs Absolute root path to use for this list. + * @param enmFmt Format of \a pcszRootPathAbs. + */ +int DnDTransferListInitEx(PDNDTRANSFERLIST pList, const char *pcszRootPathAbs, DNDTRANSFERLISTFMT enmFmt) +{ + AssertPtrReturn(pList, VERR_INVALID_POINTER); + AssertPtrReturn(pcszRootPathAbs, VERR_INVALID_POINTER); + AssertReturn(*pcszRootPathAbs, VERR_INVALID_PARAMETER); + + int rc; + + if (enmFmt == DNDTRANSFERLISTFMT_URI) + { + char *pszPath; + rc = RTUriFilePathEx(pcszRootPathAbs, RTPATH_STR_F_STYLE_UNIX, &pszPath, 0 /*cbPath*/, NULL /*pcchPath*/); + if (RT_SUCCESS(rc)) + { + rc = dndTransferListInitInternal(pList, pszPath); + RTStrFree(pszPath); + } + } + else + rc = dndTransferListInitInternal(pList, pcszRootPathAbs); + + return rc; +} + +/** + * Initializes a transfer list. + * + * @returns VBox status code. + * @param pList Transfer list to initialize. + */ +int DnDTransferListInit(PDNDTRANSFERLIST pList) +{ + return dndTransferListInitInternal(pList, NULL /* pcszRootPathAbs */); +} + +/** + * Destroys a transfer list. + * + * @param pList Transfer list to destroy. + */ +void DnDTransferListDestroy(PDNDTRANSFERLIST pList) +{ + if (!pList) + return; + + DnDTransferListReset(pList); + + RTStrFree(pList->pszPathRootAbs); + pList->pszPathRootAbs = NULL; +} + +/** + * Initializes a transfer list and sets the root path. + * + * Convenience function which calls dndTransferListInitInternal() if not initialized already. + * + * @returns VBox status code. + * @param pList List to determine root path for. + * @param pcszRootPathAbs Root path to use. + */ +static int dndTransferInitAndSetRoot(PDNDTRANSFERLIST pList, const char *pcszRootPathAbs) +{ + int rc; + + if (!pList->pszPathRootAbs) + { + rc = dndTransferListInitInternal(pList, pcszRootPathAbs); + AssertRCReturn(rc, rc); + + LogRel2(("DnD: Determined root path is '%s'\n", pList->pszPathRootAbs)); + } + else + rc = VINF_SUCCESS; + + return rc; +} + +/** + * Resets a transfer list to its initial state. + * + * @param pList Transfer list to reset. + */ +void DnDTransferListReset(PDNDTRANSFERLIST pList) +{ + AssertPtrReturnVoid(pList); + + if (!pList->pszPathRootAbs) + return; + + RTStrFree(pList->pszPathRootAbs); + pList->pszPathRootAbs = NULL; + + PDNDTRANSFERLISTROOT pRootCur, pRootNext; + RTListForEachSafe(&pList->lstRoot, pRootCur, pRootNext, DNDTRANSFERLISTROOT, Node) + dndTransferListRootEntryFree(pList, pRootCur); + Assert(RTListIsEmpty(&pList->lstRoot)); + + PDNDTRANSFEROBJECT pObjCur, pObjNext; + RTListForEachSafe(&pList->lstObj, pObjCur, pObjNext, DNDTRANSFEROBJECT, Node) + dndTransferListObjFree(pList, pObjCur); + Assert(RTListIsEmpty(&pList->lstObj)); + + Assert(pList->cRoots == 0); + Assert(pList->cObj == 0); + + pList->cbObjTotal = 0; +} + +/** + * Adds a single transfer object entry to a transfer List. + * + * @returns VBox status code. + * @param pList Transfer list to add entry to. + * @param pcszSrcAbs Absolute source path (local) to use. + * @param fMode File mode of entry to add. + * @param fFlags Transfer list flags to use for appending. + */ +static int dndTransferListObjAdd(PDNDTRANSFERLIST pList, const char *pcszSrcAbs, RTFMODE fMode, DNDTRANSFERLISTFLAGS fFlags) +{ + AssertPtrReturn(pList, VERR_INVALID_POINTER); + AssertPtrReturn(pcszSrcAbs, VERR_INVALID_POINTER); + + LogFlowFunc(("pcszSrcAbs=%s, fMode=%#x, fFlags=0x%x\n", pcszSrcAbs, fMode, fFlags)); + + int rc = VINF_SUCCESS; + + if ( !RTFS_IS_FILE(fMode) + && !RTFS_IS_DIRECTORY(fMode)) + /** @todo Symlinks not allowed. */ + { + rc = VERR_NOT_SUPPORTED; + } + + if (RT_SUCCESS(rc)) + { + /* Calculate the path to add as the destination path to our URI object. */ + const size_t idxPathToAdd = strlen(pList->pszPathRootAbs); + AssertReturn(strlen(pcszSrcAbs) > idxPathToAdd, VERR_INVALID_PARAMETER); /* Should never happen (tm). */ + + PDNDTRANSFEROBJECT pObj = (PDNDTRANSFEROBJECT)RTMemAllocZ(sizeof(DNDTRANSFEROBJECT)); + if (pObj) + { + const bool fIsFile = RTFS_IS_FILE(fMode); + + rc = DnDTransferObjectInitEx(pObj, fIsFile ? DNDTRANSFEROBJTYPE_FILE : DNDTRANSFEROBJTYPE_DIRECTORY, + pList->pszPathRootAbs, &pcszSrcAbs[idxPathToAdd]); + if (RT_SUCCESS(rc)) + { + if (fIsFile) + rc = DnDTransferObjectOpen(pObj, + RTFILE_O_OPEN | RTFILE_O_READ | RTFILE_O_DENY_WRITE, /** @todo Add a standard fOpen mode for this list. */ + 0 /* fMode */, DNDTRANSFEROBJECT_FLAGS_NONE); + if (RT_SUCCESS(rc)) + { + RTListAppend(&pList->lstObj, &pObj->Node); + + pList->cObj++; + if (fIsFile) + pList->cbObjTotal += DnDTransferObjectGetSize(pObj); + + if ( fIsFile + && !(fFlags & DNDTRANSFERLIST_FLAGS_KEEP_OPEN)) /* Shall we keep the file open while being added to this list? */ + rc = DnDTransferObjectClose(pObj); + } + + if (RT_FAILURE(rc)) + DnDTransferObjectDestroy(pObj); + } + + if (RT_FAILURE(rc)) + RTMemFree(pObj); + } + else + rc = VERR_NO_MEMORY; + } + + if (RT_FAILURE(rc)) + LogRel(("DnD: Adding entry '%s' of type %#x failed with rc=%Rrc\n", pcszSrcAbs, fMode & RTFS_TYPE_MASK, rc)); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Frees an internal DnD transfer list object. + * + * @param pList Transfer list to free object for. + * @param pLstObj transfer list object to free. The pointer will be invalid after calling. + */ +static void dndTransferListObjFree(PDNDTRANSFERLIST pList, PDNDTRANSFEROBJECT pObj) +{ + if (!pObj) + return; + + DnDTransferObjectDestroy(pObj); + RTListNodeRemove(&pObj->Node); + RTMemFree(pObj); + + AssertReturnVoid(pList->cObj); + pList->cObj--; +} + +/** + * Helper routine for handling adding sub directories. + * + * @return IPRT status code. + * @param pList transfer list to add found entries to. + * @param pszDir Pointer to the directory buffer. + * @param cchDir The length of pszDir in pszDir. + * @param pDirEntry Pointer to the directory entry. + * @param fFlags Flags of type DNDTRANSFERLISTFLAGS. + */ +static int dndTransferListAppendPathRecursiveSub(PDNDTRANSFERLIST pList, + char *pszDir, size_t cchDir, PRTDIRENTRYEX pDirEntry, + DNDTRANSFERLISTFLAGS fFlags) + +{ + Assert(cchDir > 0); Assert(pszDir[cchDir] == '\0'); + + /* Make sure we've got some room in the path, to save us extra work further down. */ + if (cchDir + 3 >= RTPATH_MAX) + return VERR_BUFFER_OVERFLOW; + + /* Open directory. */ + RTDIR hDir; + int rc = RTDirOpen(&hDir, pszDir); + if (RT_FAILURE(rc)) + return rc; + + /* Ensure we've got a trailing slash (there is space for it see above). */ + if (!RTPATH_IS_SEP(pszDir[cchDir - 1])) + { + pszDir[cchDir++] = RTPATH_SLASH; + pszDir[cchDir] = '\0'; + } + + rc = dndTransferListObjAdd(pList, pszDir, pDirEntry->Info.Attr.fMode, fFlags); + AssertRCReturn(rc, rc); + + LogFlowFunc(("pszDir=%s\n", pszDir)); + + /* + * Process the files and subdirs. + */ + for (;;) + { + /* Get the next directory. */ + size_t cbDirEntry = DNDTRANSFERLIST_DIRENTRY_BUF_SIZE; + rc = RTDirReadEx(hDir, pDirEntry, &cbDirEntry, RTFSOBJATTRADD_UNIX, RTPATH_F_ON_LINK); + if (RT_FAILURE(rc)) + break; + + /* Check length. */ + if (pDirEntry->cbName + cchDir + 3 >= RTPATH_MAX) + { + rc = VERR_BUFFER_OVERFLOW; + break; + } + + switch (pDirEntry->Info.Attr.fMode & RTFS_TYPE_MASK) + { + case RTFS_TYPE_SYMLINK: + { + if (!(fFlags & DNDTRANSFERLIST_FLAGS_RESOLVE_SYMLINKS)) + break; + RT_FALL_THRU(); + } + case RTFS_TYPE_DIRECTORY: + { + if (RTDirEntryExIsStdDotLink(pDirEntry)) + continue; + + memcpy(&pszDir[cchDir], pDirEntry->szName, pDirEntry->cbName + 1); + int rc2 = dndTransferListAppendPathRecursiveSub(pList, pszDir, cchDir + pDirEntry->cbName, pDirEntry, fFlags); + if (RT_SUCCESS(rc)) + rc = rc2; + break; + } + + case RTFS_TYPE_FILE: + { + memcpy(&pszDir[cchDir], pDirEntry->szName, pDirEntry->cbName + 1); + rc = dndTransferListObjAdd(pList, pszDir, pDirEntry->Info.Attr.fMode, fFlags); + break; + } + + default: + { + + break; + } + } + } + + if (rc == VERR_NO_MORE_FILES) /* Done reading current directory? */ + { + rc = VINF_SUCCESS; + } + else if (RT_FAILURE(rc)) + LogRel(("DnD: Error while adding files recursively, rc=%Rrc\n", rc)); + + int rc2 = RTDirClose(hDir); + if (RT_FAILURE(rc2)) + { + if (RT_SUCCESS(rc)) + rc = rc2; + } + + return rc; +} + +/** + * Appends a native system path recursively by adding these entries as transfer objects. + * + * @returns VBox status code. + * @param pList Transfer list to add found entries to. + * @param pcszPathAbs Absolute path to add. + * @param fFlags Flags of type DNDTRANSFERLISTFLAGS. + */ +static int dndTransferListAppendDirectoryRecursive(PDNDTRANSFERLIST pList, + const char *pcszPathAbs, DNDTRANSFERLISTFLAGS fFlags) +{ + char szPathAbs[RTPATH_MAX]; + int rc = RTStrCopy(szPathAbs, sizeof(szPathAbs), pcszPathAbs); + if (RT_FAILURE(rc)) + return rc; + + union + { + uint8_t abPadding[DNDTRANSFERLIST_DIRENTRY_BUF_SIZE]; + RTDIRENTRYEX DirEntry; + } uBuf; + + const size_t cchPathAbs = RTStrNLen(szPathAbs, RTPATH_MAX); + AssertReturn(cchPathAbs, VERR_BUFFER_OVERFLOW); + + /* Use the directory entry to hand-in the directorie's information. */ + rc = RTPathQueryInfo(pcszPathAbs, &uBuf.DirEntry.Info, RTFSOBJATTRADD_NOTHING); + AssertRCReturn(rc, rc); + + return dndTransferListAppendPathRecursiveSub(pList, szPathAbs, cchPathAbs, &uBuf.DirEntry, fFlags); +} + +/** + * Helper function for appending a local directory to a DnD transfer list. + * + * @returns VBox status code. + * @param pList Transfer list to return total number of root entries for. + * @param pszPathAbs Absolute path of directory to append. + * @param cbPathAbs Size (in bytes) of absolute path of directory to append. + * @param pObjInfo Pointer to directory object info to append. + * @param fFlags Transfer list flags to use for appending. + */ +static int dndTransferListAppendDirectory(PDNDTRANSFERLIST pList, char* pszPathAbs, size_t cbPathAbs, + PRTFSOBJINFO pObjInfo, DNDTRANSFERLISTFLAGS fFlags) +{ + const size_t cchPathRoot = RTStrNLen(pList->pszPathRootAbs, RTPATH_MAX); + AssertReturn(cchPathRoot, VERR_INVALID_PARAMETER); + + const size_t cchPathAbs = RTPathEnsureTrailingSeparator(pszPathAbs, sizeof(cbPathAbs)); + AssertReturn(cchPathAbs, VERR_BUFFER_OVERFLOW); + AssertReturn(cchPathAbs >= cchPathRoot, VERR_BUFFER_UNDERFLOW); + + const bool fPathIsRoot = cchPathAbs == cchPathRoot; + + int rc; + + if (!fPathIsRoot) + { + rc = dndTransferListObjAdd(pList, pszPathAbs, pObjInfo->Attr.fMode, fFlags); + AssertRCReturn(rc, rc); + } + + RTDIR hDir; + rc = RTDirOpen(&hDir, pszPathAbs); + AssertRCReturn(rc, rc); + + for (;;) + { + /* Get the next entry. */ + RTDIRENTRYEX dirEntry; + rc = RTDirReadEx(hDir, &dirEntry, NULL, RTFSOBJATTRADD_UNIX, + RTPATH_F_ON_LINK /** @todo No symlinks yet. */); + if (RT_SUCCESS(rc)) + { + if (RTDirEntryExIsStdDotLink(&dirEntry)) + continue; + + /* Check length. */ + if (dirEntry.cbName + cchPathAbs + 3 >= cbPathAbs) + { + rc = VERR_BUFFER_OVERFLOW; + break; + } + + /* Append the directory entry to our absolute path. */ + memcpy(&pszPathAbs[cchPathAbs], dirEntry.szName, dirEntry.cbName + 1 /* Include terminator */); + + LogFlowFunc(("szName=%s, pszPathAbs=%s\n", dirEntry.szName, pszPathAbs)); + + switch (dirEntry.Info.Attr.fMode & RTFS_TYPE_MASK) + { + case RTFS_TYPE_DIRECTORY: + { + if (fFlags & DNDTRANSFERLIST_FLAGS_RECURSIVE) + rc = dndTransferListAppendDirectoryRecursive(pList, pszPathAbs, fFlags); + break; + } + + case RTFS_TYPE_FILE: + { + rc = dndTransferListObjAdd(pList, pszPathAbs, dirEntry.Info.Attr.fMode, fFlags); + break; + } + + default: + /* Silently skip everything else. */ + break; + } + + if ( RT_SUCCESS(rc) + /* Make sure to add a root entry if we're processing the root path at the moment. */ + && fPathIsRoot) + { + rc = dndTransferListRootEntryAdd(pList, pszPathAbs); + } + } + else if (rc == VERR_NO_MORE_FILES) + { + rc = VINF_SUCCESS; + break; + } + else + break; + } + + RTDirClose(hDir); + return rc; +} + +/** + * Appends a native path to a DnD transfer list. + * + * @returns VBox status code. + * @param pList Transfer list to append native path to. + * @param pcszPath Path (native) to append. + * @param fFlags Transfer list flags to use for appending. + */ +static int dndTransferListAppendPathNative(PDNDTRANSFERLIST pList, const char *pcszPath, DNDTRANSFERLISTFLAGS fFlags) +{ + /* We don't want to have a relative directory here. */ + if (!RTPathStartsWithRoot(pcszPath)) + return VERR_INVALID_PARAMETER; + + int rc = DnDPathValidate(pcszPath, false /* fMustExist */); + AssertRCReturn(rc, rc); + + char szPathAbs[RTPATH_MAX]; + size_t cbPathAbs = sizeof(szPathAbs); + rc = RTStrCopy(szPathAbs, cbPathAbs, pcszPath); + AssertRCReturn(rc, rc); + + size_t cchPathAbs = RTStrNLen(szPathAbs, cbPathAbs); + AssertReturn(cchPathAbs, VERR_INVALID_PARAMETER); + + /* Convert path to transport style. */ + rc = DnDPathConvert(szPathAbs, cbPathAbs, DNDPATHCONVERT_FLAGS_TRANSPORT); + if (RT_SUCCESS(rc)) + { + /* Make sure the path has the same root path as our list. */ + if (RTPathStartsWith(szPathAbs, pList->pszPathRootAbs)) + { + RTFSOBJINFO objInfo; + rc = RTPathQueryInfo(szPathAbs, &objInfo, RTFSOBJATTRADD_NOTHING); + if (RT_SUCCESS(rc)) + { + const uint32_t fType = objInfo.Attr.fMode & RTFS_TYPE_MASK; + + if ( RTFS_IS_DIRECTORY(fType) + || RTFS_IS_FILE(fType)) + { + if (RTFS_IS_DIRECTORY(fType)) + { + cchPathAbs = RTPathEnsureTrailingSeparator(szPathAbs, cbPathAbs); + AssertReturn(cchPathAbs, VERR_BUFFER_OVERFLOW); + } + + const size_t cchPathRoot = RTStrNLen(pList->pszPathRootAbs, RTPATH_MAX); + AssertStmt(cchPathRoot, rc = VERR_INVALID_PARAMETER); + + if ( RT_SUCCESS(rc) + && cchPathAbs > cchPathRoot) + rc = dndTransferListRootEntryAdd(pList, szPathAbs); + } + else + rc = VERR_NOT_SUPPORTED; + + if (RT_SUCCESS(rc)) + { + switch (fType) + { + case RTFS_TYPE_DIRECTORY: + { + rc = dndTransferListAppendDirectory(pList, szPathAbs, cbPathAbs, &objInfo, fFlags); + break; + } + + case RTFS_TYPE_FILE: + { + rc = dndTransferListObjAdd(pList, szPathAbs, objInfo.Attr.fMode, fFlags); + break; + } + + default: + AssertFailed(); + break; + } + } + } + /* On UNIX-y OSes RTPathQueryInfo() returns VERR_FILE_NOT_FOUND in some cases + * so tweak this to make it uniform to Windows. */ + else if (rc == VERR_FILE_NOT_FOUND) + rc = VERR_PATH_NOT_FOUND; + } + else + rc = VERR_INVALID_PARAMETER; + } + + if (RT_FAILURE(rc)) + LogRel(("DnD: Adding native path '%s' failed with rc=%Rrc\n", pcszPath, rc)); + + return rc; +} + +/** + * Appends an URI path to a DnD transfer list. + * + * @returns VBox status code. + * @param pList Transfer list to append native path to. + * @param pcszPath URI path to append. + * @param fFlags Transfer list flags to use for appending. + */ +static int dndTransferListAppendPathURI(PDNDTRANSFERLIST pList, const char *pcszPath, DNDTRANSFERLISTFLAGS fFlags) +{ + RT_NOREF(fFlags); + + /* Query the path component of a file URI. If this hasn't a + * file scheme, NULL is returned. */ + char *pszPath; + int rc = RTUriFilePathEx(pcszPath, RTPATH_STR_F_STYLE_UNIX, &pszPath, 0 /*cbPath*/, NULL /*pcchPath*/); + if (RT_SUCCESS(rc)) + { + rc = dndTransferListAppendPathNative(pList, pszPath, fFlags); + RTStrFree(pszPath); + } + + if (RT_FAILURE(rc)) + LogRel(("DnD: Adding URI path '%s' failed with rc=%Rrc\n", pcszPath, rc)); + + return rc; +} + +/** + * Appends a single path to a transfer list. + * + * @returns VBox status code. VERR_NOT_SUPPORTED if the path is not supported. + * @param pList Transfer list to append to. + * @param enmFmt Format of \a pszPaths to append. + * @param pcszPath Path to append. Must be part of the list's set root path. + * @param fFlags Transfer list flags to use for appending. + */ +int DnDTransferListAppendPath(PDNDTRANSFERLIST pList, + DNDTRANSFERLISTFMT enmFmt, const char *pcszPath, DNDTRANSFERLISTFLAGS fFlags) +{ + AssertPtrReturn(pList, VERR_INVALID_POINTER); + AssertPtrReturn(pcszPath, VERR_INVALID_POINTER); + AssertReturn(!(fFlags & ~DNDTRANSFERLIST_FLAGS_VALID_MASK), VERR_INVALID_FLAGS); + AssertReturn(!(fFlags & DNDTRANSFERLIST_FLAGS_RESOLVE_SYMLINKS), VERR_NOT_SUPPORTED); + + int rc; + + switch (enmFmt) + { + case DNDTRANSFERLISTFMT_NATIVE: + rc = dndTransferListAppendPathNative(pList, pcszPath, fFlags); + break; + + case DNDTRANSFERLISTFMT_URI: + rc = dndTransferListAppendPathURI(pList, pcszPath, fFlags); + break; + + default: + AssertFailedStmt(rc = VERR_NOT_SUPPORTED); + break; /* Never reached */ + } + + return rc; +} + +/** + * Appends native paths to a transfer list. + * + * @returns VBox status code. + * @param pList Transfer list to append to. + * @param enmFmt Format of \a pszPaths to append. + * @param pszPaths Buffer of paths to append. + * @param cbPaths Size (in bytes) of buffer of paths to append. + * @param pcszSeparator Separator string to use. + * @param fFlags Transfer list flags to use for appending. + */ +int DnDTransferListAppendPathsFromBuffer(PDNDTRANSFERLIST pList, + DNDTRANSFERLISTFMT enmFmt, const char *pszPaths, size_t cbPaths, + const char *pcszSeparator, DNDTRANSFERLISTFLAGS fFlags) +{ + AssertPtrReturn(pList, VERR_INVALID_POINTER); + AssertPtrReturn(pszPaths, VERR_INVALID_POINTER); + AssertReturn(cbPaths, VERR_INVALID_PARAMETER); + + char **papszPaths = NULL; + size_t cPaths = 0; + int rc = RTStrSplit(pszPaths, cbPaths, pcszSeparator, &papszPaths, &cPaths); + if (RT_SUCCESS(rc)) + rc = DnDTransferListAppendPathsFromArray(pList, enmFmt, papszPaths, cPaths, fFlags); + + for (size_t i = 0; i < cPaths; ++i) + RTStrFree(papszPaths[i]); + RTMemFree(papszPaths); + + return rc; +} + +/** + * Appends paths to a transfer list. + * + * @returns VBox status code. Will return VERR_INVALID_PARAMETER if a common root path could not be found. + * @param pList Transfer list to append path to. + * @param enmFmt Format of \a papcszPaths to append. + * @param papcszPaths Array of paths to append. + * @param cPaths Number of paths in \a papcszPaths to append. + * @param fFlags Transfer list flags to use for appending. + */ +int DnDTransferListAppendPathsFromArray(PDNDTRANSFERLIST pList, + DNDTRANSFERLISTFMT enmFmt, + const char * const *papcszPaths, size_t cPaths, DNDTRANSFERLISTFLAGS fFlags) +{ + AssertPtrReturn(pList, VERR_INVALID_POINTER); + AssertPtrReturn(papcszPaths, VERR_INVALID_POINTER); + AssertReturn(!(fFlags & ~DNDTRANSFERLIST_FLAGS_VALID_MASK), VERR_INVALID_FLAGS); + + int rc = VINF_SUCCESS; + + if (!cPaths) /* Nothing to add? Bail out. */ + return VINF_SUCCESS; + + char **papszPathsTmp = NULL; + + /* If URI data is being handed in, extract the paths first. */ + if (enmFmt == DNDTRANSFERLISTFMT_URI) + { + papszPathsTmp = (char **)RTMemAlloc(sizeof(char *) * cPaths); + if (papszPathsTmp) + { + for (size_t i = 0; i < cPaths; i++) + papszPathsTmp[i] = RTUriFilePath(papcszPaths[i]); + } + else + rc = VERR_NO_MEMORY; + } + + if (RT_FAILURE(rc)) + return rc; + + /* If we don't have a root path set, try to find the common path of all handed-in paths. */ + if (!pList->pszPathRootAbs) + { + /* Can we work on the unmodified, handed-in data or do we need to use our temporary paths? */ + const char * const *papszPathTmp = enmFmt == DNDTRANSFERLISTFMT_NATIVE + ? papcszPaths : papszPathsTmp; + + size_t cchRootPath = 0; /* Length of root path in chars. */ + if (cPaths > 1) + { + cchRootPath = RTPathFindCommon(cPaths, papszPathTmp); + } + else + cchRootPath = RTPathParentLength(papszPathTmp[0]); + + if (cchRootPath) + { + /* Just use the first path in the array as the reference. */ + char *pszRootPath = RTStrDupN(papszPathTmp[0], cchRootPath); + if (pszRootPath) + { + rc = dndTransferInitAndSetRoot(pList, pszRootPath); + RTStrFree(pszRootPath); + } + else + rc = VERR_NO_MEMORY; + } + else + rc = VERR_INVALID_PARAMETER; + } + + if (RT_SUCCESS(rc)) + { + /* + * Add all paths to the list. + */ + for (size_t i = 0; i < cPaths; i++) + { + const char *pcszPath = enmFmt == DNDTRANSFERLISTFMT_NATIVE + ? papcszPaths[i] : papszPathsTmp[i]; + rc = DnDTransferListAppendPath(pList, DNDTRANSFERLISTFMT_NATIVE, pcszPath, fFlags); + if (RT_FAILURE(rc)) + { + LogRel(("DnD: Adding path '%s' (format %#x, root '%s') to transfer list failed with %Rrc\n", + pcszPath, enmFmt, pList->pszPathRootAbs ? pList->pszPathRootAbs : "", rc)); + break; + } + } + } + + if (papszPathsTmp) + { + for (size_t i = 0; i < cPaths; i++) + RTStrFree(papszPathsTmp[i]); + RTMemFree(papszPathsTmp); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Appends the root entries for a transfer list. + * + * @returns VBox status code. + * @param pList Transfer list to append to. + * @param enmFmt Format of \a pszPaths to append. + * @param pszPaths Buffer of paths to append. + * @param cbPaths Size (in bytes) of buffer of paths to append. + * @param pcszSeparator Separator string to use. + * @param fFlags Transfer list flags to use for appending. + */ +int DnDTransferListAppendRootsFromBuffer(PDNDTRANSFERLIST pList, + DNDTRANSFERLISTFMT enmFmt, const char *pszPaths, size_t cbPaths, + const char *pcszSeparator, DNDTRANSFERLISTFLAGS fFlags) +{ + AssertPtrReturn(pList, VERR_INVALID_POINTER); + AssertPtrReturn(pszPaths, VERR_INVALID_POINTER); + AssertReturn(cbPaths, VERR_INVALID_PARAMETER); + + char **papszPaths = NULL; + size_t cPaths = 0; + int rc = RTStrSplit(pszPaths, cbPaths, pcszSeparator, &papszPaths, &cPaths); + if (RT_SUCCESS(rc)) + rc = DnDTransferListAppendRootsFromArray(pList, enmFmt, papszPaths, cPaths, fFlags); + + for (size_t i = 0; i < cPaths; ++i) + RTStrFree(papszPaths[i]); + RTMemFree(papszPaths); + + return rc; +} + +/** + * Appends root entries to a transfer list. + * + * @returns VBox status code. + * @param pList Transfer list to append root entries to. + * @param enmFmt Format of \a papcszPaths to append. + * @param papcszPaths Array of paths to append. + * @param cPaths Number of paths in \a papcszPaths to append. + * @param fFlags Transfer list flags to use for appending. + */ +int DnDTransferListAppendRootsFromArray(PDNDTRANSFERLIST pList, + DNDTRANSFERLISTFMT enmFmt, + const char * const *papcszPaths, size_t cPaths, DNDTRANSFERLISTFLAGS fFlags) +{ + AssertPtrReturn(pList, VERR_INVALID_POINTER); + AssertPtrReturn(papcszPaths, VERR_INVALID_POINTER); + AssertReturn(!(fFlags & ~DNDTRANSFERLIST_FLAGS_VALID_MASK), VERR_INVALID_FLAGS); + + AssertMsgReturn(pList->pszPathRootAbs, ("Root path not set yet\n"), VERR_WRONG_ORDER); + + int rc = VINF_SUCCESS; + + if (!cPaths) /* Nothing to add? Bail out. */ + return VINF_SUCCESS; + + char **papszPathsTmp = NULL; + + /* If URI data is being handed in, extract the paths first. */ + if (enmFmt == DNDTRANSFERLISTFMT_URI) + { + papszPathsTmp = (char **)RTMemAlloc(sizeof(char *) * cPaths); + if (papszPathsTmp) + { + for (size_t i = 0; i < cPaths; i++) + papszPathsTmp[i] = RTUriFilePath(papcszPaths[i]); + } + else + rc = VERR_NO_MEMORY; + } + + if (RT_FAILURE(rc)) + return rc; + + char szPath[RTPATH_MAX]; + + /* + * Add all root entries to the root list. + */ + for (size_t i = 0; i < cPaths; i++) + { + const char *pcszPath = enmFmt == DNDTRANSFERLISTFMT_NATIVE + ? papcszPaths[i] : papszPathsTmp[i]; + + rc = RTPathJoin(szPath, sizeof(szPath), pList->pszPathRootAbs, pcszPath); + AssertRCBreak(rc); + + rc = DnDPathConvert(szPath, sizeof(szPath), DNDPATHCONVERT_FLAGS_TRANSPORT); + AssertRCBreak(rc); + + rc = dndTransferListRootEntryAdd(pList, szPath); + if (RT_FAILURE(rc)) + { + LogRel(("DnD: Adding root entry '%s' (format %#x, root '%s') to transfer list failed with %Rrc\n", + szPath, enmFmt, pList->pszPathRootAbs, rc)); + break; + } + } + + if (papszPathsTmp) + { + for (size_t i = 0; i < cPaths; i++) + RTStrFree(papszPathsTmp[i]); + RTMemFree(papszPathsTmp); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Returns the first transfer object in a list. + * + * @returns Pointer to transfer object if found, or NULL if not found. + * @param pList Transfer list to get first transfer object from. + */ +PDNDTRANSFEROBJECT DnDTransferListObjGetFirst(PDNDTRANSFERLIST pList) +{ + AssertPtrReturn(pList, NULL); + + return RTListGetFirst(&pList->lstObj, DNDTRANSFEROBJECT, Node); +} + +/** + * Removes an object from a transfer list, internal version. + * + * @param pList Transfer list to remove object from. + * @param pObj Object to remove. The object will be free'd and the pointer is invalid after calling. + */ +static void dndTransferListObjRemoveInternal(PDNDTRANSFERLIST pList, PDNDTRANSFEROBJECT pObj) +{ + AssertPtrReturnVoid(pList); + AssertPtrReturnVoid(pObj); + + /** @todo Validate if \a pObj is part of \a pList. */ + + uint64_t cbSize = DnDTransferObjectGetSize(pObj); + Assert(pList->cbObjTotal >= cbSize); + pList->cbObjTotal -= cbSize; /* Adjust total size. */ + + dndTransferListObjFree(pList, pObj); +} + +/** + * Removes an object from a transfer list. + * + * @param pList Transfer list to remove object from. + * @param pObj Object to remove. The object will be free'd and the pointer is invalid after calling. + */ +void DnDTransferListObjRemove(PDNDTRANSFERLIST pList, PDNDTRANSFEROBJECT pObj) +{ + return dndTransferListObjRemoveInternal(pList, pObj); +} + +/** + * Removes the first DnD transfer object from a transfer list. + * + * @param pList Transfer list to remove first entry for. + */ +void DnDTransferListObjRemoveFirst(PDNDTRANSFERLIST pList) +{ + AssertPtrReturnVoid(pList); + + if (!pList->cObj) + return; + + PDNDTRANSFEROBJECT pObj = RTListGetFirst(&pList->lstObj, DNDTRANSFEROBJECT, Node); + AssertPtr(pObj); + + dndTransferListObjRemoveInternal(pList, pObj); +} + +/** + * Returns all root entries of a transfer list as a string. + * + * @returns VBox status code. + * @param pList Transfer list to return root paths for. + * @param pcszPathBase Root path to use as a base path. If NULL, the list's absolute root path will be used (if any). + * @param pcszSeparator Separator to use for separating the root entries. + * @param ppszBuffer Where to return the allocated string on success. Needs to be free'd with RTStrFree(). + * @param pcbBuffer Where to return the size (in bytes) of the allocated string on success, including terminator. + */ +int DnDTransferListGetRootsEx(PDNDTRANSFERLIST pList, + DNDTRANSFERLISTFMT enmFmt, const char *pcszPathBase, const char *pcszSeparator, + char **ppszBuffer, size_t *pcbBuffer) +{ + AssertPtrReturn(pList, VERR_INVALID_POINTER); + /* pcszPathBase can be NULL. */ + AssertPtrReturn(pcszSeparator, VERR_INVALID_POINTER); + AssertPtrReturn(ppszBuffer, VERR_INVALID_POINTER); + AssertPtrReturn(pcbBuffer, VERR_INVALID_POINTER); + + char *pszString = NULL; + size_t cchString = 0; + + const size_t cchSep = RTStrNLen(pcszSeparator, RTPATH_MAX); + + /* Find out which root path to use. */ + const char *pcszPathRootTmp = pcszPathBase ? pcszPathBase : pList->pszPathRootAbs; + /* pcszPathRootTmp can be NULL */ + + LogFlowFunc(("Using root path '%s'\n", pcszPathRootTmp ? pcszPathRootTmp : "")); + + int rc = DnDPathValidate(pcszPathRootTmp, false /* fMustExist */); + if (RT_FAILURE(rc)) + return rc; + + char szPath[RTPATH_MAX]; + + PDNDTRANSFERLISTROOT pRoot; + RTListForEach(&pList->lstRoot, pRoot, DNDTRANSFERLISTROOT, Node) + { + if (pcszPathRootTmp) + { + rc = RTStrCopy(szPath, sizeof(szPath), pcszPathRootTmp); + AssertRCBreak(rc); + } + + rc = RTPathAppend(szPath, sizeof(szPath), pRoot->pszPathRoot); + AssertRCBreak(rc); + + if (enmFmt == DNDTRANSFERLISTFMT_URI) + { + char *pszPathURI = RTUriFileCreate(szPath); + AssertPtrBreakStmt(pszPathURI, rc = VERR_NO_MEMORY); + rc = RTStrAAppend(&pszString, pszPathURI); + cchString += RTStrNLen(pszPathURI, RTPATH_MAX); + RTStrFree(pszPathURI); + AssertRCBreak(rc); + } + else /* Native */ + { +#if RTPATH_STYLE == RTPATH_STR_F_STYLE_DOS + /* Convert paths to native path style. */ + rc = DnDPathConvert(szPath, sizeof(szPath), DNDPATHCONVERT_FLAGS_TO_DOS); +#endif + if (RT_SUCCESS(rc)) + { + rc = RTStrAAppend(&pszString, szPath); + AssertRCBreak(rc); + + cchString += RTStrNLen(szPath, RTPATH_MAX); + } + } + + rc = RTStrAAppend(&pszString, pcszSeparator); + AssertRCBreak(rc); + + cchString += cchSep; + } + + if (RT_SUCCESS(rc)) + { + *ppszBuffer = pszString; + *pcbBuffer = pszString ? cchString + 1 /* Include termination */ : 0; + } + else + RTStrFree(pszString); + return rc; +} + +/** + * Returns all root entries for a DnD transfer list. + * + * Note: Convenience function which uses the default DnD path separator. + * + * @returns VBox status code. + * @param pList Transfer list to return root entries for. + * @param enmFmt Which format to use for returning the entries. + * @param ppszBuffer Where to return the allocated string on success. Needs to be free'd with RTStrFree(). + * @param pcbBuffer Where to return the size (in bytes) of the allocated string on success, including terminator. + */ +int DnDTransferListGetRoots(PDNDTRANSFERLIST pList, + DNDTRANSFERLISTFMT enmFmt, char **ppszBuffer, size_t *pcbBuffer) +{ + return DnDTransferListGetRootsEx(pList, enmFmt, "" /* pcszPathRoot */, DND_PATH_SEPARATOR_STR, + ppszBuffer, pcbBuffer); +} + +/** + * Returns the total root entries count for a DnD transfer list. + * + * @returns Total number of root entries. + * @param pList Transfer list to return total number of root entries for. + */ +uint64_t DnDTransferListGetRootCount(PDNDTRANSFERLIST pList) +{ + AssertPtrReturn(pList, 0); + return pList->cRoots; +} + +/** + * Returns the absolute root path for a DnD transfer list. + * + * @returns Pointer to the root path. + * @param pList Transfer list to return root path for. + */ +const char *DnDTransferListGetRootPathAbs(PDNDTRANSFERLIST pList) +{ + AssertPtrReturn(pList, NULL); + return pList->pszPathRootAbs; +} + +/** + * Returns the total transfer object count for a DnD transfer list. + * + * @returns Total number of transfer objects. + * @param pList Transfer list to return total number of transfer objects for. + */ +uint64_t DnDTransferListObjCount(PDNDTRANSFERLIST pList) +{ + AssertPtrReturn(pList, 0); + return pList->cObj; +} + +/** + * Returns the total bytes of all handled transfer objects for a DnD transfer list. + * + * @returns VBox status code. + * @param pList Transfer list to return total bytes for. + */ +uint64_t DnDTransferListObjTotalBytes(PDNDTRANSFERLIST pList) +{ + AssertPtrReturn(pList, 0); + return pList->cbObjTotal; +} + +/** + * Sets the absolute root path of a transfer list. + * + * @returns VBox status code. + * @param pList Transfer list to set root path for. + * @param pcszRootPathAbs Absolute root path to set. + */ +static int dndTransferListSetRootPath(PDNDTRANSFERLIST pList, const char *pcszRootPathAbs) +{ + AssertPtrReturn(pList, VERR_INVALID_POINTER); + AssertPtrReturn(pcszRootPathAbs, VERR_INVALID_POINTER); + AssertReturn(pList->pszPathRootAbs == NULL, VERR_WRONG_ORDER); /* Already initialized? */ + + LogFlowFunc(("pcszRootPathAbs=%s\n", pcszRootPathAbs)); + + char szRootPath[RTPATH_MAX]; + int rc = RTStrCopy(szRootPath, sizeof(szRootPath), pcszRootPathAbs); + if (RT_FAILURE(rc)) + return rc; + + /* Note: The list's root path is kept in native style, so no conversion needed here. */ + + RTPathEnsureTrailingSeparatorEx(szRootPath, sizeof(szRootPath), RTPATH_STR_F_STYLE_HOST); + + /* Make sure the root path is a directory (and no symlink or stuff). */ + RTFSOBJINFO objInfo; + rc = RTPathQueryInfo(szRootPath, &objInfo, RTFSOBJATTRADD_NOTHING); + if (RT_SUCCESS(rc)) + { + if (RTFS_IS_DIRECTORY(objInfo.Attr.fMode & RTFS_TYPE_MASK)) + { + pList->pszPathRootAbs = RTStrDup(szRootPath); + if (pList->pszPathRootAbs) + { + LogFlowFunc(("Root path is '%s'\n", pList->pszPathRootAbs)); + } + else + rc = VERR_NO_MEMORY; + } + else + rc = VERR_NOT_A_DIRECTORY; + } + + return rc; +} + +/** + * Adds a root entry to a DnD transfer list. + * + * @returns VBox status code. + * @param pList Transfer list to add root entry to. + * @param pcszRoot Root entry to add. + */ +static int dndTransferListRootEntryAdd(PDNDTRANSFERLIST pList, const char *pcszRoot) +{ + AssertPtrReturn(pList->pszPathRootAbs, VERR_WRONG_ORDER); /* The list's root path must be set first. */ + + int rc; + + /** @todo Handle / reject double entries. */ + + /* Get the index pointing to the relative path in relation to set the root path. */ + const size_t idxPathToAdd = strlen(pList->pszPathRootAbs); + AssertReturn(strlen(pcszRoot) > idxPathToAdd, VERR_INVALID_PARAMETER); /* Should never happen (tm). */ + + PDNDTRANSFERLISTROOT pRoot = (PDNDTRANSFERLISTROOT)RTMemAllocZ(sizeof(DNDTRANSFERLISTROOT)); + if (pRoot) + { + const char *pcszRootIdx = &pcszRoot[idxPathToAdd]; + + LogFlowFunc(("pcszRoot=%s\n", pcszRootIdx)); + + pRoot->pszPathRoot = RTStrDup(pcszRootIdx); + if (pRoot->pszPathRoot) + { + RTListAppend(&pList->lstRoot, &pRoot->Node); + pList->cRoots++; + + rc = VINF_SUCCESS; + } + else + rc = VERR_NO_MEMORY; + + if (RT_FAILURE(rc)) + { + RTMemFree(pRoot); + pRoot = NULL; + } + } + else + rc = VERR_NO_MEMORY; + + return rc; +} + +/** + * Removes (and destroys) a DnD transfer root entry. + * + * @param pList Transfer list to free root for. + * @param pRootObj Transfer list root to free. The pointer will be invalid after calling. + */ +static void dndTransferListRootEntryFree(PDNDTRANSFERLIST pList, PDNDTRANSFERLISTROOT pRootObj) +{ + if (!pRootObj) + return; + + RTStrFree(pRootObj->pszPathRoot); + + RTListNodeRemove(&pRootObj->Node); + RTMemFree(pRootObj); + + AssertReturnVoid(pList->cRoots); + pList->cRoots--; +} + diff --git a/src/VBox/GuestHost/DragAndDrop/DnDTransferObject.cpp b/src/VBox/GuestHost/DragAndDrop/DnDTransferObject.cpp new file mode 100644 index 00000000..eadaa395 --- /dev/null +++ b/src/VBox/GuestHost/DragAndDrop/DnDTransferObject.cpp @@ -0,0 +1,706 @@ +/* $Id: DnDTransferObject.cpp $ */ +/** @file + * DnD - Transfer object implemenation for handling creation/reading/writing to files and directories on host or guest side. + */ + +/* + * Copyright (C) 2014-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_GUEST_DND +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + + +/********************************************************************************************************************************* +* Prototypes * +*********************************************************************************************************************************/ +static int dndTransferObjectCloseInternal(PDNDTRANSFEROBJECT pObj); +static int dndTransferObjectQueryInfoInternal(PDNDTRANSFEROBJECT pObj); + + +/** + * Initializes the object, internal version. + * + * @returns VBox status code. + * @param pObj DnD transfer object to initialize. + */ +static int dndTransferObjectInitInternal(PDNDTRANSFEROBJECT pObj) +{ + AssertPtrReturn(pObj, VERR_INVALID_POINTER); + + pObj->enmType = DNDTRANSFEROBJTYPE_UNKNOWN; + pObj->idxDst = 0; + pObj->pszPath = NULL; + + RT_ZERO(pObj->u); + + return VINF_SUCCESS; +} + +/** + * Initializes the object. + * + * @returns VBox status code. + * @param pObj DnD transfer object to initialize. + */ +int DnDTransferObjectInit(PDNDTRANSFEROBJECT pObj) +{ + return dndTransferObjectInitInternal(pObj); +} + +/** + * Initializes the object with an expected object type and file path. + * + * @returns VBox status code. + * @param pObj DnD transfer object to initialize. + * @param enmType Type we expect this object to be. + * @param pcszPathSrcAbs Absolute source (local) path of file this object represents. Can be empty (e.g. for root stuff). + * @param pcszPathDst Relative path of file this object represents at the destination. + * Together with \a pcszPathSrcAbs this represents the complete absolute local path. + */ +int DnDTransferObjectInitEx(PDNDTRANSFEROBJECT pObj, + DNDTRANSFEROBJTYPE enmType, const char *pcszPathSrcAbs, const char *pcszPathDst) +{ + AssertPtrReturn(pObj, VERR_INVALID_POINTER); + AssertReturn(pObj->enmType == DNDTRANSFEROBJTYPE_UNKNOWN, VERR_WRONG_ORDER); /* Already initialized? */ + /* pcszPathSrcAbs can be empty. */ + AssertPtrReturn(pcszPathDst, VERR_INVALID_POINTER); + + int rc = dndTransferObjectInitInternal(pObj); + AssertRCReturn(rc, rc); + + rc = DnDPathValidate(pcszPathDst, false /* Does not need to exist */); + AssertRCReturn(rc, rc); + + char szPath[RTPATH_MAX + 1]; + + /* Save the index (in characters) where the first destination segment starts. */ + if ( pcszPathSrcAbs + && RTStrNLen(pcszPathSrcAbs, RTSTR_MAX)) + { + rc = DnDPathValidate(pcszPathSrcAbs, false /* Does not need to exist */); + if (RT_FAILURE(rc)) + return rc; + + rc = RTStrCopy(szPath, sizeof(szPath), pcszPathSrcAbs); + if (RT_SUCCESS(rc)) + rc = RTPathEnsureTrailingSeparator(szPath, sizeof(szPath)) == 0 ? VERR_BUFFER_OVERFLOW : VINF_SUCCESS; + + /* Save the index (in characters) where the destination part starts. */ + pObj->idxDst = (uint16_t)RTStrNLen(szPath, RTPATH_MAX); + AssertReturn(pObj->idxDst <= RTPATH_MAX, VERR_INVALID_PARAMETER); + } + else + { + szPath[0] = '\0'; /* Init empty string. */ + pObj->idxDst = 0; + } + + if (RT_FAILURE(rc)) + return rc; + + /* Append the destination part. */ + rc = RTPathAppend(szPath, sizeof(szPath), pcszPathDst); + if ( RT_SUCCESS(rc) + && enmType == DNDTRANSFEROBJTYPE_DIRECTORY) + rc = RTPathEnsureTrailingSeparator(szPath, sizeof(szPath)) == 0 ? VERR_BUFFER_OVERFLOW : VINF_SUCCESS; + + if (RT_FAILURE(rc)) + return rc; + + pObj->pszPath = RTStrDup(szPath); + if (!pObj->pszPath) + return VERR_NO_MEMORY; + + /* Convert paths into transport format. */ + rc = DnDPathConvert(pObj->pszPath, strlen(pObj->pszPath), DNDPATHCONVERT_FLAGS_TRANSPORT); + if (RT_FAILURE(rc)) + { + RTStrFree(pObj->pszPath); + pObj->pszPath = NULL; + return rc; + } + + LogFlowFunc(("enmType=%RU32, pcszPathSrcAbs=%s, pcszPathDst=%s -> pszPath=%s\n", + enmType, pcszPathSrcAbs, pcszPathDst, pObj->pszPath)); + + pObj->enmType = enmType; + + return VINF_SUCCESS; +} + +/** + * Destroys a DnD transfer object. + * + * @param pObj DnD transfer object to destroy. + */ +void DnDTransferObjectDestroy(PDNDTRANSFEROBJECT pObj) +{ + if (!pObj) + return; + + DnDTransferObjectReset(pObj); +} + +/** + * Closes the object's internal handles (to files / ...). + * + * @returns VBox status code. + * @param pObj DnD transfer object to close internally. + */ +static int dndTransferObjectCloseInternal(PDNDTRANSFEROBJECT pObj) +{ + AssertPtrReturn(pObj, VERR_INVALID_POINTER); + + int rc = VINF_SUCCESS; + + if (pObj->pszPath) + LogRel2(("DnD: Closing '%s'\n", pObj->pszPath)); + + switch (pObj->enmType) + { + case DNDTRANSFEROBJTYPE_FILE: + { + if (RTFileIsValid(pObj->u.File.hFile)) + { + rc = RTFileClose(pObj->u.File.hFile); + if (RT_SUCCESS(rc)) + { + pObj->u.File.hFile = NIL_RTFILE; + RT_ZERO(pObj->u.File.objInfo); + } + else + LogRel(("DnD: Closing file '%s' failed with %Rrc\n", pObj->pszPath, rc)); + } + break; + } + + case DNDTRANSFEROBJTYPE_DIRECTORY: + { + if (RTDirIsValid(pObj->u.Dir.hDir)) + { + rc = RTDirClose(pObj->u.Dir.hDir); + if (RT_SUCCESS(rc)) + { + pObj->u.Dir.hDir = NIL_RTDIR; + RT_ZERO(pObj->u.Dir.objInfo); + } + else + LogRel(("DnD: Closing directory '%s' failed with %Rrc\n", pObj->pszPath, rc)); + } + break; + } + + default: + break; + } + + return rc; +} + +/** + * Closes the object. + * This also closes the internal handles associated with the object (to files / ...). + * + * @returns VBox status code. + * @param pObj DnD transfer object to close. + */ +int DnDTransferObjectClose(PDNDTRANSFEROBJECT pObj) +{ + AssertPtrReturn(pObj, VERR_INVALID_POINTER); + + return dndTransferObjectCloseInternal(pObj); +} + +/** + * Returns the absolute source path of the object. + * + * @return Absolute source path of the object. + * @param pObj DnD transfer object to get source path for. + */ +const char *DnDTransferObjectGetSourcePath(PDNDTRANSFEROBJECT pObj) +{ + AssertPtrReturn(pObj, NULL); + return pObj->pszPath; +} + +/** + * Returns the (relative) destination path of the object, in transport style. + * + * @return Relative destination path of the object, or NULL if not set. + * @param pObj DnD transfer object to get destination path for. + */ +const char *DnDTransferObjectGetDestPath(PDNDTRANSFEROBJECT pObj) +{ + AssertPtrReturn(pObj, NULL); + + if (!pObj->pszPath) + return NULL; + + AssertReturn(strlen(pObj->pszPath) >= pObj->idxDst, NULL); + + return &pObj->pszPath[pObj->idxDst]; +} + +/** + * Returns the (relative) destination path of the object, extended version. + * + * @return VBox status code, or VERR_NOT_FOUND if not initialized yet. + * @param pObj DnD transfer object to get destination path for. + * @param enmStyle Which path style to return. + * @param pszBuf Where to store the path. + * @param cbBuf Size (in bytes) where to store the path. + */ +int DnDTransferObjectGetDestPathEx(PDNDTRANSFEROBJECT pObj, DNDTRANSFEROBJPATHSTYLE enmStyle, char *pszBuf, size_t cbBuf) +{ + AssertPtrReturn(pObj, VERR_INVALID_POINTER); + AssertPtrReturn(pszBuf, VERR_INVALID_POINTER); + AssertReturn(cbBuf, VERR_INVALID_PARAMETER); + + if (!pObj->pszPath) + return VERR_NOT_FOUND; + + AssertReturn(strlen(pObj->pszPath) >= pObj->idxDst, VERR_INTERNAL_ERROR); + + int rc = RTStrCopy(pszBuf, cbBuf, &pObj->pszPath[pObj->idxDst]); + if ( RT_SUCCESS(rc) + && enmStyle == DNDTRANSFEROBJPATHSTYLE_DOS) + rc = DnDPathConvert(pszBuf, cbBuf, DNDPATHCONVERT_FLAGS_TO_DOS); + + return rc; +} + +/** + * Returns the directory / file mode of the object. + * + * @return File / directory mode. + * @param pObj DnD transfer object to get directory / file mode for. + */ +RTFMODE DnDTransferObjectGetMode(PDNDTRANSFEROBJECT pObj) +{ + AssertPtrReturn(pObj, 0); + + switch (pObj->enmType) + { + case DNDTRANSFEROBJTYPE_FILE: + return pObj->u.File.objInfo.Attr.fMode; + + case DNDTRANSFEROBJTYPE_DIRECTORY: + return pObj->u.Dir.objInfo.Attr.fMode; + + default: + break; + } + + return 0; +} + +/** + * Returns the bytes already processed (read / written). + * + * Note: Only applies if the object is of type DnDTransferObjectType_File. + * + * @return Bytes already processed (read / written). + * @param pObj DnD transfer object to get processed bytes for. + */ +uint64_t DnDTransferObjectGetProcessed(PDNDTRANSFEROBJECT pObj) +{ + if (pObj->enmType == DNDTRANSFEROBJTYPE_FILE) + return pObj->u.File.cbProcessed; + + return 0; +} + +/** + * Returns the file's logical size (in bytes). + * + * Note: Only applies if the object is of type DnDTransferObjectType_File. + * + * @return The file's logical size (in bytes). + * @param pObj DnD transfer object to get size for. + */ +uint64_t DnDTransferObjectGetSize(PDNDTRANSFEROBJECT pObj) +{ + if (pObj->enmType == DNDTRANSFEROBJTYPE_FILE) + return pObj->u.File.cbToProcess; + + return 0; +} + +/** + * Returns the object's type. + * + * @return The object's type. + * @param pObj DnD transfer object to get type for. + */ +DNDTRANSFEROBJTYPE DnDTransferObjectGetType(PDNDTRANSFEROBJECT pObj) +{ + return pObj->enmType; +} + +/** + * Returns whether the processing of the object is complete or not. + * For file objects this means that all bytes have been processed. + * + * @return True if complete, False if not. + * @param pObj DnD transfer object to get completion status for. + */ +bool DnDTransferObjectIsComplete(PDNDTRANSFEROBJECT pObj) +{ + bool fComplete; + + switch (pObj->enmType) + { + case DNDTRANSFEROBJTYPE_FILE: + Assert(pObj->u.File.cbProcessed <= pObj->u.File.cbToProcess); + fComplete = pObj->u.File.cbProcessed == pObj->u.File.cbToProcess; + break; + + case DNDTRANSFEROBJTYPE_DIRECTORY: + fComplete = true; + break; + + default: + fComplete = true; + break; + } + + return fComplete; +} + +/** + * Returns whether the object is in an open state or not. + * @param pObj DnD transfer object to get open status for. + */ +bool DnDTransferObjectIsOpen(PDNDTRANSFEROBJECT pObj) +{ + switch (pObj->enmType) + { + case DNDTRANSFEROBJTYPE_FILE: return RTFileIsValid(pObj->u.File.hFile); + case DNDTRANSFEROBJTYPE_DIRECTORY: return RTDirIsValid(pObj->u.Dir.hDir); + default: break; + } + + return false; +} + +/** + * Open the object with a specific file type, and, depending on the type, specifying additional parameters. + * + * @return IPRT status code. + * @param pObj DnD transfer object to open. + * @param fOpen Open mode to use; only valid for file objects. + * @param fMode File mode to set; only valid for file objects. Depends on fOpen and and can be 0. + * @param fFlags Additional DnD transfer object flags. + */ +int DnDTransferObjectOpen(PDNDTRANSFEROBJECT pObj, uint64_t fOpen, RTFMODE fMode, DNDTRANSFEROBJECTFLAGS fFlags) +{ + AssertPtrReturn(pObj, VERR_INVALID_POINTER); + AssertReturn(fOpen, VERR_INVALID_FLAGS); + /* fMode is optional. */ + AssertReturn(!(fFlags & ~DNDTRANSFEROBJECT_FLAGS_VALID_MASK), VERR_INVALID_FLAGS); + RT_NOREF1(fFlags); + + int rc = VINF_SUCCESS; + + LogFlowFunc(("pszPath=%s, fOpen=0x%x, fMode=0x%x, fFlags=0x%x\n", pObj->pszPath, fOpen, fMode, fFlags)); + + switch (pObj->enmType) + { + case DNDTRANSFEROBJTYPE_FILE: + { + LogRel2(("DnD: Opening file '%s'\n", pObj->pszPath)); + + /* + * Open files on the source with RTFILE_O_DENY_WRITE to prevent races + * where the OS writes to the file while the destination side transfers + * it over. + */ + rc = RTFileOpen(&pObj->u.File.hFile, pObj->pszPath, fOpen); + if (RT_SUCCESS(rc)) + { + if ( (fOpen & RTFILE_O_WRITE) /* Only set the file mode on write. */ + && fMode /* Some file mode to set specified? */) + { + rc = RTFileSetMode(pObj->u.File.hFile, fMode); + if (RT_FAILURE(rc)) + LogRel(("DnD: Setting mode %#x for file '%s' failed with %Rrc\n", fMode, pObj->pszPath, rc)); + } + else if (fOpen & RTFILE_O_READ) + { + rc = dndTransferObjectQueryInfoInternal(pObj); + } + } + else + LogRel(("DnD: Opening file '%s' failed with %Rrc\n", pObj->pszPath, rc)); + + if (RT_SUCCESS(rc)) + { + LogFlowFunc(("File cbObject=%RU64, fMode=0x%x\n", + pObj->u.File.objInfo.cbObject, pObj->u.File.objInfo.Attr.fMode)); + pObj->u.File.cbToProcess = pObj->u.File.objInfo.cbObject; + pObj->u.File.cbProcessed = 0; + } + + break; + } + + case DNDTRANSFEROBJTYPE_DIRECTORY: + { + LogRel2(("DnD: Opening directory '%s'\n", pObj->pszPath)); + + rc = RTDirOpen(&pObj->u.Dir.hDir, pObj->pszPath); + if (RT_SUCCESS(rc)) + { + rc = dndTransferObjectQueryInfoInternal(pObj); + } + else + LogRel(("DnD: Opening directory '%s' failed with %Rrc\n", pObj->pszPath, rc)); + break; + } + + default: + rc = VERR_NOT_IMPLEMENTED; + break; + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Queries information about the object using a specific view, internal version. + * + * @return IPRT status code. + * @param pObj DnD transfer object to query info for. + */ +static int dndTransferObjectQueryInfoInternal(PDNDTRANSFEROBJECT pObj) +{ + int rc; + + switch (pObj->enmType) + { + case DNDTRANSFEROBJTYPE_FILE: + AssertMsgReturn(RTFileIsValid(pObj->u.File.hFile), ("Object has invalid file handle\n"), VERR_INVALID_STATE); + rc = RTFileQueryInfo(pObj->u.File.hFile, &pObj->u.File.objInfo, RTFSOBJATTRADD_NOTHING); + break; + + case DNDTRANSFEROBJTYPE_DIRECTORY: + AssertMsgReturn(RTDirIsValid(pObj->u.Dir.hDir), ("Object has invalid directory handle\n"), VERR_INVALID_STATE); + rc = RTDirQueryInfo(pObj->u.Dir.hDir, &pObj->u.Dir.objInfo, RTFSOBJATTRADD_NOTHING); + break; + + default: + rc = VERR_NOT_IMPLEMENTED; + break; + } + + if (RT_FAILURE(rc)) + LogRel(("DnD: Querying information for '%s' failed with %Rrc\n", pObj->pszPath, rc)); + + return rc; +} + +/** + * Queries information about the object using a specific view. + * + * @return IPRT status code. + * @param pObj DnD transfer object to query info for. + */ +int DnDTransferObjectQueryInfo(PDNDTRANSFEROBJECT pObj) +{ + AssertPtrReturn(pObj, VERR_INVALID_POINTER); + return dndTransferObjectQueryInfoInternal(pObj); +} + +/** + * Reads data from the object. Only applies to files objects. + * + * @return IPRT status code. + * @param pObj DnD transfer object to read data from. + * @param pvBuf Buffer where to store the read data. + * @param cbBuf Size (in bytes) of the buffer. + * @param pcbRead Pointer where to store how many bytes were read. Optional. + */ +int DnDTransferObjectRead(PDNDTRANSFEROBJECT pObj, void *pvBuf, size_t cbBuf, uint32_t *pcbRead) +{ + AssertPtrReturn(pObj, VERR_INVALID_POINTER); + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + AssertReturn(cbBuf, VERR_INVALID_PARAMETER); + /* pcbRead is optional. */ + + size_t cbRead = 0; + + int rc; + switch (pObj->enmType) + { + case DNDTRANSFEROBJTYPE_FILE: + { + rc = RTFileRead(pObj->u.File.hFile, pvBuf, cbBuf, &cbRead); + if (RT_SUCCESS(rc)) + { + pObj->u.File.cbProcessed += cbRead; + Assert(pObj->u.File.cbProcessed <= pObj->u.File.cbToProcess); + + /* End of file reached or error occurred? */ + if ( pObj->u.File.cbToProcess + && pObj->u.File.cbProcessed == pObj->u.File.cbToProcess) + { + rc = VINF_EOF; + } + } + else + LogRel(("DnD: Reading from file '%s' failed with %Rrc\n", pObj->pszPath, rc)); + break; + } + + case DNDTRANSFEROBJTYPE_DIRECTORY: + { + rc = VINF_SUCCESS; + break; + } + + default: + rc = VERR_NOT_IMPLEMENTED; + break; + } + + if (RT_SUCCESS(rc)) + { + if (pcbRead) + *pcbRead = (uint32_t)cbRead; + } + + LogFlowFunc(("Returning cbRead=%zu, rc=%Rrc\n", cbRead, rc)); + return rc; +} + +/** + * Resets the object's state and closes all related handles. + * + * @param pObj DnD transfer object to reset. + */ +void DnDTransferObjectReset(PDNDTRANSFEROBJECT pObj) +{ + AssertPtrReturnVoid(pObj); + + LogFlowFuncEnter(); + + int vrc2 = dndTransferObjectCloseInternal(pObj); + AssertRCReturnVoid(vrc2); + + pObj->enmType = DNDTRANSFEROBJTYPE_UNKNOWN; + pObj->idxDst = 0; + + RTStrFree(pObj->pszPath); + pObj->pszPath = NULL; + + RT_ZERO(pObj->u); +} + +/** + * Sets the bytes to process by the object. + * + * Note: Only applies if the object is of type DnDTransferObjectType_File. + * + * @return IPRT return code. + * @param pObj DnD transfer object to set size for. + * @param cbSize Size (in bytes) to process. + */ +int DnDTransferObjectSetSize(PDNDTRANSFEROBJECT pObj, uint64_t cbSize) +{ + AssertPtrReturn(pObj, VERR_INVALID_POINTER); + AssertReturn(pObj->enmType == DNDTRANSFEROBJTYPE_FILE, VERR_INVALID_PARAMETER); + + /** @todo Implement sparse file support here. */ + + pObj->u.File.cbToProcess = cbSize; + return VINF_SUCCESS; +} + +/** + * Writes data to an object. Only applies to file objects. + * + * @return IPRT status code. + * @param pObj DnD transfer object to write to. + * @param pvBuf Buffer of data to write. + * @param cbBuf Size (in bytes) of data to write. + * @param pcbWritten Pointer where to store how many bytes were written. Optional. + */ +int DnDTransferObjectWrite(PDNDTRANSFEROBJECT pObj, const void *pvBuf, size_t cbBuf, uint32_t *pcbWritten) +{ + AssertPtrReturn(pObj, VERR_INVALID_POINTER); + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + AssertReturn(cbBuf, VERR_INVALID_PARAMETER); + /* pcbWritten is optional. */ + + size_t cbWritten = 0; + + int rc; + switch (pObj->enmType) + { + case DNDTRANSFEROBJTYPE_FILE: + { + rc = RTFileWrite(pObj->u.File.hFile, pvBuf, cbBuf, &cbWritten); + if (RT_SUCCESS(rc)) + { + pObj->u.File.cbProcessed += cbWritten; + } + else + LogRel(("DnD: Writing to file '%s' failed with %Rrc\n", pObj->pszPath, rc)); + break; + } + + case DNDTRANSFEROBJTYPE_DIRECTORY: + { + rc = VINF_SUCCESS; + break; + } + + default: + rc = VERR_NOT_IMPLEMENTED; + break; + } + + if (RT_SUCCESS(rc)) + { + if (pcbWritten) + *pcbWritten = (uint32_t)cbWritten; + } + + LogFlowFunc(("Returning cbWritten=%zu, rc=%Rrc\n", cbWritten, rc)); + return rc; +} + diff --git a/src/VBox/GuestHost/DragAndDrop/DnDUtils.cpp b/src/VBox/GuestHost/DragAndDrop/DnDUtils.cpp new file mode 100644 index 00000000..43fba52a --- /dev/null +++ b/src/VBox/GuestHost/DragAndDrop/DnDUtils.cpp @@ -0,0 +1,177 @@ +/* $Id: DnDUtils.cpp $ */ +/** @file + * DnD - Common utility functions. + */ + +/* + * Copyright (C) 2022-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include +#include + +#include +#include + +using namespace DragAndDropSvc; + +/** + * Converts a host HGCM message to a string. + * + * @returns Stringified version of the host message. + */ +const char *DnDHostMsgToStr(uint32_t uMsg) +{ + switch (uMsg) + { + RT_CASE_RET_STR(HOST_DND_FN_SET_MODE); + RT_CASE_RET_STR(HOST_DND_FN_CANCEL); + RT_CASE_RET_STR(HOST_DND_FN_HG_EVT_ENTER); + RT_CASE_RET_STR(HOST_DND_FN_HG_EVT_MOVE); + RT_CASE_RET_STR(HOST_DND_FN_HG_EVT_LEAVE); + RT_CASE_RET_STR(HOST_DND_FN_HG_EVT_DROPPED); + RT_CASE_RET_STR(HOST_DND_FN_HG_SND_DATA_HDR); + RT_CASE_RET_STR(HOST_DND_FN_HG_SND_DATA); + RT_CASE_RET_STR(HOST_DND_FN_HG_SND_MORE_DATA); + RT_CASE_RET_STR(HOST_DND_FN_HG_SND_DIR); + RT_CASE_RET_STR(HOST_DND_FN_HG_SND_FILE_DATA); + RT_CASE_RET_STR(HOST_DND_FN_HG_SND_FILE_HDR); + RT_CASE_RET_STR(HOST_DND_FN_GH_REQ_PENDING); + RT_CASE_RET_STR(HOST_DND_FN_GH_EVT_DROPPED); + default: + break; + } + return "unknown"; +} + +/** + * Converts a guest HGCM message to a string. + * + * @returns Stringified version of the guest message. + */ +const char *DnDGuestMsgToStr(uint32_t uMsg) +{ + switch (uMsg) + { + RT_CASE_RET_STR(GUEST_DND_FN_CONNECT); + RT_CASE_RET_STR(GUEST_DND_FN_DISCONNECT); + RT_CASE_RET_STR(GUEST_DND_FN_REPORT_FEATURES); + RT_CASE_RET_STR(GUEST_DND_FN_QUERY_FEATURES); + RT_CASE_RET_STR(GUEST_DND_FN_GET_NEXT_HOST_MSG); + RT_CASE_RET_STR(GUEST_DND_FN_EVT_ERROR); + RT_CASE_RET_STR(GUEST_DND_FN_HG_ACK_OP); + RT_CASE_RET_STR(GUEST_DND_FN_HG_REQ_DATA); + RT_CASE_RET_STR(GUEST_DND_FN_HG_EVT_PROGRESS); + RT_CASE_RET_STR(GUEST_DND_FN_GH_ACK_PENDING); + RT_CASE_RET_STR(GUEST_DND_FN_GH_SND_DATA_HDR); + RT_CASE_RET_STR(GUEST_DND_FN_GH_SND_DATA); + RT_CASE_RET_STR(GUEST_DND_FN_GH_SND_DIR); + RT_CASE_RET_STR(GUEST_DND_FN_GH_SND_FILE_DATA); + RT_CASE_RET_STR(GUEST_DND_FN_GH_SND_FILE_HDR); + default: + break; + } + return "unknown"; +} + +/** + * Converts a VBOXDNDACTION to a string. + * + * @returns Stringified version of VBOXDNDACTION + * @param uAction DnD action to convert. + */ +const char *DnDActionToStr(VBOXDNDACTION uAction) +{ + switch (uAction) + { + case VBOX_DND_ACTION_IGNORE: return "ignore"; + case VBOX_DND_ACTION_COPY: return "copy"; + case VBOX_DND_ACTION_MOVE: return "move"; + case VBOX_DND_ACTION_LINK: return "link"; + default: + break; + } + AssertMsgFailedReturn(("Unknown uAction=%d\n", uAction), "bad"); +} + +/** + * Converts a VBOXDNDACTIONLIST to a string. + * + * @returns Stringified version of VBOXDNDACTIONLIST. Must be free'd by the caller using RTStrFree(). + * @retval NULL on allocation failure. + * @retval "" if no (valid) actions found. + * @param fActionList DnD action list to convert. + */ +char *DnDActionListToStrA(VBOXDNDACTIONLIST fActionList) +{ + char *pszList = NULL; + +#define HANDLE_ACTION(a_Action) \ + if (fActionList & a_Action) \ + { \ + if (pszList) \ + AssertRCReturn(RTStrAAppend(&pszList, ", "), NULL); \ + AssertRCReturn(RTStrAAppend(&pszList, DnDActionToStr(a_Action)), NULL); \ + } + + HANDLE_ACTION(VBOX_DND_ACTION_IGNORE); + HANDLE_ACTION(VBOX_DND_ACTION_COPY); + HANDLE_ACTION(VBOX_DND_ACTION_MOVE); + HANDLE_ACTION(VBOX_DND_ACTION_LINK); + +#undef HANDLE_ACTION + + if (!pszList) + AssertRCReturn(RTStrAAppend(&pszList, ""), NULL); + + return pszList; +} + +/** + * Converts a VBOXDNDSTATE to a string. + * + * @returns Stringified version of VBOXDNDSTATE. + * @param enmState DnD state to convert. + */ +const char *DnDStateToStr(VBOXDNDSTATE enmState) +{ + switch (enmState) + { + case VBOXDNDSTATE_UNKNOWN: return "unknown"; + case VBOXDNDSTATE_ENTERED: return "entered VM window"; + case VBOXDNDSTATE_LEFT: return "left VM window"; + case VBOXDNDSTATE_QUERY_FORMATS: return "querying formats"; + case VBOXDNDSTATE_QUERY_STATUS: return "querying status"; + case VBOXDNDSTATE_DRAGGING: return "dragging"; + case VBOXDNDSTATE_DROP_STARTED: return "drop started"; + case VBOXDNDSTATE_DROP_ENDED: return "drop ended"; + case VBOXDNDSTATE_CANCELLED: return "cancelled"; + case VBOXDNDSTATE_ERROR: return "error"; + default: + break; + } + AssertMsgFailedReturn(("Unknown enmState=%d\n", enmState), "bad"); +} + diff --git a/src/VBox/GuestHost/DragAndDrop/Makefile.kmk b/src/VBox/GuestHost/DragAndDrop/Makefile.kmk new file mode 100644 index 00000000..91fffe5a --- /dev/null +++ b/src/VBox/GuestHost/DragAndDrop/Makefile.kmk @@ -0,0 +1,68 @@ +# $Id: Makefile.kmk $ +## @file +# Sub-Makefile for the shared DnD code for both, host and guest. +# + +# +# Copyright (C) 2014-2023 Oracle and/or its affiliates. +# +# This file is part of VirtualBox base platform packages, as +# available from https://www.virtualbox.org. +# +# 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, in version 3 of the +# License. +# +# 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, see . +# +# SPDX-License-Identifier: GPL-3.0-only +# + +SUB_DEPTH = ../../../.. +include $(KBUILD_PATH)/subheader.kmk + +ifdef VBOX_WITH_TESTCASES + include $(PATH_SUB_CURRENT)/testcase/Makefile.kmk +endif + +VBOX_PATH_HOSTSERVICES_COMMON = $(PATH_ROOT)/src/VBox/HostServices/common + +# +# DnDGuestR3Lib - For tools on the guest side, +# e.g. VBoxClient/VBoxTray. +# +ifdef VBOX_WITH_ADDITIONS + LIBRARIES += VBoxDnDGuestR3Lib +endif +VBoxDnDGuestR3Lib_TEMPLATE = VBoxGuestR3Lib +VBoxDnDGuestR3Lib_SOURCES = \ + DnDDroppedFiles.cpp \ + DnDMIME.cpp \ + DnDPath.cpp \ + DnDTransferObject.cpp \ + DnDTransferList.cpp \ + DnDUtils.cpp + +# LIBRARIES.win.amd64 += VBoxDnDGuestR3Lib-x86 +# VBoxDnDGuestR3Lib-x86_EXTENDS := VBoxDnDGuestR3Lib +# VBoxDnDGuestR3Lib-x86_BLD_TRG_ARCH := x86 + +# +# DnDHostR3Lib - For the host side, e.g. Main +# and frontends. +# +LIBRARIES += VBoxDnDHostR3Lib +VBoxDnDHostR3Lib_TEMPLATE = VBoxR3Dll +VBoxDnDHostR3Lib_SOURCES = \ + $(VBoxDnDGuestR3Lib_SOURCES) \ + $(VBOX_PATH_HOSTSERVICES_COMMON)/client.cpp \ + $(VBOX_PATH_HOSTSERVICES_COMMON)/message.cpp + +include $(FILE_KBUILD_SUB_FOOTER) diff --git a/src/VBox/GuestHost/DragAndDrop/testcase/Makefile.kmk b/src/VBox/GuestHost/DragAndDrop/testcase/Makefile.kmk new file mode 100644 index 00000000..05568ce3 --- /dev/null +++ b/src/VBox/GuestHost/DragAndDrop/testcase/Makefile.kmk @@ -0,0 +1,81 @@ +# $Id: Makefile.kmk $ +## @file +# Sub-Makefile for the DnD testcases. +# + +# +# Copyright (C) 2020-2023 Oracle and/or its affiliates. +# +# This file is part of VirtualBox base platform packages, as +# available from https://www.virtualbox.org. +# +# 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, in version 3 of the +# License. +# +# 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, see . +# +# SPDX-License-Identifier: GPL-3.0-only +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + +if defined(VBOX_WITH_TESTCASES) && !defined(VBOX_ONLY_ADDITIONS) && !defined(VBOX_ONLY_SDK) + + if defined(VBOX_WITH_QTGUI) + include $(PATH_SUB_CURRENT)/tstDragAndDropQt/Makefile.kmk + endif + + PROGRAMS += tstDnDTransferObject tstDnDTransferList tstDnDPath + TESTING += \ + $(tstDnDTransferObject_0_OUTDIR)/tstDnDTransferObject.run \ + $(tstDnDTransferList_0_OUTDIR)/tstDnDTransferList.run \ + $(tstDnDTransferPath_0_OUTDIR)/tstDnDPath.run + + tstDnDTransferObject_TEMPLATE = VBoxR3TstExe + tstDnDTransferObject_DEFS = UNIT_TEST TESTCASE + tstDnDTransferObject_SOURCES = \ + tstDnDTransferObject.cpp \ + ../DnDTransferObject.cpp \ + ../DnDPath.cpp + tstDnDTransferObject_CLEAN = $(tstDnDTransferObject_0_OUTDIR)/tstDnDTransferObject.run + + $$(tstDnDTransferObject_0_OUTDIR)/tstDnDTransferObject.run: $$(tstDnDTransferObject_1_STAGE_TARGET) + export VBOX_LOG_DEST=nofile; $(tstDnDTransferObject_1_STAGE_TARGET) quiet + $(QUIET)$(APPEND) -t "$@" "done" + + tstDnDTransferList_TEMPLATE = VBoxR3TstExe + tstDnDTransferList_DEFS = UNIT_TEST TESTCASE + tstDnDTransferList_SOURCES = \ + tstDnDTransferList.cpp \ + ../DnDTransferObject.cpp \ + ../DnDTransferList.cpp \ + ../DnDPath.cpp + tstDnDTransferList_CLEAN = $(tstDnDTransferList_0_OUTDIR)/tstDnDTransferList.run + + $$(tstDnDTransferList_0_OUTDIR)/tstDnDTransferList.run: $$(tstDnDTransferList_1_STAGE_TARGET) + export VBOX_LOG_DEST=nofile; $(tstDnDTransferList_1_STAGE_TARGET) quiet + $(QUIET)$(APPEND) -t "$@" "done" + + tstDnDPath_TEMPLATE = VBoxR3TstExe + tstDnDPath_DEFS = UNIT_TEST TESTCASE + tstDnDPath_SOURCES = \ + tstDnDPath.cpp \ + ../DnDPath.cpp + tstDnDPath_CLEAN = $(tstDnDPath_0_OUTDIR)/tstDnDPath.run + + $$(tstDnDPath_0_OUTDIR)/tstDnDPath.run: $$(tstDnDPath_1_STAGE_TARGET) + export VBOX_LOG_DEST=nofile; $(tstDnDPath_1_STAGE_TARGET) quiet + $(QUIET)$(APPEND) -t "$@" "done" + +endif + +include $(FILE_KBUILD_SUB_FOOTER) diff --git a/src/VBox/GuestHost/DragAndDrop/testcase/tstDnDPath.cpp b/src/VBox/GuestHost/DragAndDrop/testcase/tstDnDPath.cpp new file mode 100644 index 00000000..8cface1b --- /dev/null +++ b/src/VBox/GuestHost/DragAndDrop/testcase/tstDnDPath.cpp @@ -0,0 +1,100 @@ +/* $Id: tstDnDPath.cpp $ */ +/** @file + * DnD path tests. + */ + +/* + * Copyright (C) 2020-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include +#include +#include +#include +#include +#include + +#include + + +static void tstPathRebase(RTTEST hTest) +{ + static struct + { + char const *pszPath; + char const *pszPathOld; + char const *pszPathNew; + int rc; + char const *pszResult; + } const s_aTests[] = { + /* Invalid stuff. */ + { NULL, NULL, NULL, VERR_INVALID_POINTER, NULL }, + { "foo", "old", NULL, VERR_INVALID_POINTER, NULL }, + /* Actual rebasing. */ + { "old/foo", "old", "new", VINF_SUCCESS, "new/foo" }, + /* Note: DnDPathRebase intentionally does not do any path conversions. */ +#ifdef RT_OS_WINDOWS + { "old\\foo", "old", "new", VINF_SUCCESS, "new/foo" }, + { "\\totally\\different\\path\\foo", "/totally/different/path", "/totally/different/path", VINF_SUCCESS, "/totally/different/path/foo" }, + { "\\old\\path\\foo", "", "/new/root/", VINF_SUCCESS, "/new/root/old/path/foo" }, + { "\\\\old\\path\\\\foo", "", "/new/root/", VINF_SUCCESS, "/new/root/old/path\\\\foo" } +#else + { "old/foo", "old", "new", VINF_SUCCESS, "new/foo" }, + { "/totally/different/path/foo", "/totally/different/path", "/totally/different/path", VINF_SUCCESS, "/totally/different/path/foo" }, + { "/old/path/foo", "", "/new/root/", VINF_SUCCESS, "/new/root/old/path/foo" }, + { "//old/path//foo", "", "/new/root/", VINF_SUCCESS, "/new/root/old/path//foo" } +#endif + }; + + char *pszPath = NULL; + for (size_t i = 0; i < RT_ELEMENTS(s_aTests); i++) + { + RTTestDisableAssertions(hTest); + RTTEST_CHECK_RC(hTest, DnDPathRebase(s_aTests[i].pszPath, s_aTests[i].pszPathOld, s_aTests[i].pszPathNew, &pszPath), + s_aTests[i].rc); + RTTestRestoreAssertions(hTest); + if (RT_SUCCESS(s_aTests[i].rc)) + { + if (s_aTests[i].pszResult) + RTTEST_CHECK_MSG(hTest, RTPathCompare(pszPath, s_aTests[i].pszResult) == 0, + (hTest, "Test #%zu failed: Got '%s', expected '%s'", i, pszPath, s_aTests[i].pszResult)); + RTStrFree(pszPath); + pszPath = NULL; + } + } +} + +int main() +{ + /* + * Init the runtime, test and say hello. + */ + RTTEST hTest; + int rc = RTTestInitAndCreate("tstDnDPath", &hTest); + if (rc) + return rc; + RTTestBanner(hTest); + + tstPathRebase(hTest); + + return RTTestSummaryAndDestroy(hTest); +} + diff --git a/src/VBox/GuestHost/DragAndDrop/testcase/tstDnDTransferList.cpp b/src/VBox/GuestHost/DragAndDrop/testcase/tstDnDTransferList.cpp new file mode 100644 index 00000000..164b6200 --- /dev/null +++ b/src/VBox/GuestHost/DragAndDrop/testcase/tstDnDTransferList.cpp @@ -0,0 +1,206 @@ +/* $Id: tstDnDTransferList.cpp $ */ +/** @file + * DnD transfer list tests. + */ + +/* + * Copyright (C) 2020-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include +#include +#include +#include +#include +#include + +#include + + +int main() +{ + /* + * Init the runtime, test and say hello. + */ + RTTEST hTest; + int rc = RTTestInitAndCreate("tstDnDTransferList", &hTest); + if (rc) + return rc; + RTTestBanner(hTest); + + char szPathWellKnown[RTPATH_MAX]; + RTStrCopy(szPathWellKnown, sizeof(szPathWellKnown), +#ifdef RT_OS_WINDOWS + "C:\\Windows\\System32\\Boot\\"); +#else + "/bin/"); +#endif + + char szPathWellKnownURI[RTPATH_MAX]; + RTStrPrintf(szPathWellKnownURI, sizeof(szPathWellKnownURI), "file:///%s", szPathWellKnown); + + DNDTRANSFERLIST list; + RT_ZERO(list); + + /* Invalid stuff. */ + RTTestDisableAssertions(hTest); + RTTEST_CHECK_RC(hTest, DnDTransferListInitEx(&list, "", DNDTRANSFERLISTFMT_NATIVE), VERR_INVALID_PARAMETER); + RTTEST_CHECK_RC(hTest, DnDTransferListInitEx(&list, szPathWellKnown, DNDTRANSFERLISTFMT_NATIVE), VINF_SUCCESS); + RTTEST_CHECK_RC(hTest, DnDTransferListInitEx(&list, szPathWellKnown, DNDTRANSFERLISTFMT_NATIVE), VERR_WRONG_ORDER); + RTTestRestoreAssertions(hTest); + DnDTransferListDestroy(&list); + + /* Empty. */ + RTTEST_CHECK_RC(hTest, DnDTransferListInit(&list), VINF_SUCCESS); + DnDTransferListDestroy(&list); + + /* Initial status. */ + RTTEST_CHECK_RC(hTest, DnDTransferListInitEx(&list, szPathWellKnown, DNDTRANSFERLISTFMT_NATIVE), VINF_SUCCESS); + RTTEST_CHECK(hTest, DnDTransferListGetRootCount(&list) == 0); + RTTEST_CHECK(hTest, DnDTransferListObjCount(&list) == 0); + RTTEST_CHECK(hTest, DnDTransferListObjTotalBytes(&list) == 0); + RTTEST_CHECK(hTest, DnDTransferListObjGetFirst(&list) == NULL); + DnDTransferListDestroy(&list); + + char szPathTest[RTPATH_MAX]; + + /* Root path handling. */ + RTTEST_CHECK_RC(hTest, DnDTransferListInitEx(&list, szPathWellKnown, DNDTRANSFERLISTFMT_NATIVE), VINF_SUCCESS); + RTTEST_CHECK_RC(hTest, DnDTransferListAppendPath(&list, DNDTRANSFERLISTFMT_NATIVE, "/wrong/root/path", DNDTRANSFERLIST_FLAGS_NONE), VERR_INVALID_PARAMETER); + rc = RTPathJoin(szPathTest, sizeof(szPathTest), szPathWellKnown, "/non/existing"); + AssertRCReturn(rc, RTEXITCODE_FAILURE); + RTTEST_CHECK_RC(hTest, DnDTransferListAppendPath(&list, DNDTRANSFERLISTFMT_NATIVE, szPathTest, DNDTRANSFERLIST_FLAGS_NONE), VERR_PATH_NOT_FOUND); + DnDTransferListDestroy(&list); + + /* Adding native stuff. */ + /* No root path set yet and non-recursive -> will set root path to szPathWellKnown, but without any entries added. */ + RTTEST_CHECK_RC(hTest, DnDTransferListInitEx(&list, szPathWellKnown, DNDTRANSFERLISTFMT_NATIVE), VINF_SUCCESS); + RTTEST_CHECK_RC(hTest, DnDTransferListAppendPath(&list, DNDTRANSFERLISTFMT_NATIVE, szPathWellKnown, DNDTRANSFERLIST_FLAGS_NONE), VINF_SUCCESS); + RTTEST_CHECK(hTest, DnDTransferListGetRootCount(&list)); + RTTEST_CHECK(hTest, DnDTransferListObjCount(&list)); + + /* Add szPathWellKnown again, this time recursively. */ + RTTEST_CHECK_RC(hTest, DnDTransferListAppendPath(&list, DNDTRANSFERLISTFMT_NATIVE, szPathWellKnown, DNDTRANSFERLIST_FLAGS_RECURSIVE), VINF_SUCCESS); + RTTEST_CHECK(hTest, DnDTransferListGetRootCount(&list)); + RTTEST_CHECK(hTest, DnDTransferListObjCount(&list)); + + char *pszString = NULL; + size_t cbString = 0; + RTTEST_CHECK_RC_OK(hTest, DnDTransferListGetRoots(&list, DNDTRANSFERLISTFMT_NATIVE, &pszString, &cbString)); + RTTestPrintf(hTest, RTTESTLVL_DEBUG, "Roots:\n%s\n\n", pszString); + RTStrFree(pszString); + + PDNDTRANSFEROBJECT pObj; + while ((pObj = DnDTransferListObjGetFirst(&list))) + { + RTTestPrintf(hTest, RTTESTLVL_DEBUG, "Obj: %s\n", DnDTransferObjectGetDestPath(pObj)); + DnDTransferListObjRemoveFirst(&list); + } + DnDTransferListDestroy(&list); + + char *pszBuf; + size_t cbBuf; + + /* To URI data. */ + RTTEST_CHECK_RC(hTest, DnDTransferListInitEx(&list, szPathWellKnownURI, DNDTRANSFERLISTFMT_URI), VINF_SUCCESS); + RTStrPrintf(szPathTest, sizeof(szPathTest), "%s/foo", szPathWellKnownURI); + RTTEST_CHECK_RC(hTest, DnDTransferListAppendPath(&list, DNDTRANSFERLISTFMT_URI, szPathWellKnownURI, DNDTRANSFERLIST_FLAGS_NONE), VINF_SUCCESS); + RTTEST_CHECK_RC(hTest, DnDTransferListAppendPath(&list, DNDTRANSFERLISTFMT_URI, szPathTest, DNDTRANSFERLIST_FLAGS_NONE), VERR_PATH_NOT_FOUND); + RTTEST_CHECK_RC(hTest, DnDTransferListGetRootsEx(&list, DNDTRANSFERLISTFMT_NATIVE, "" /* pszBasePath */, "\n", &pszBuf, &cbBuf), VINF_SUCCESS); + RTTestPrintf(hTest, RTTESTLVL_DEBUG, "Roots (native):\n%s\n", pszBuf); + RTStrFree(pszBuf); + RTTEST_CHECK_RC(hTest, DnDTransferListGetRootsEx(&list, DNDTRANSFERLISTFMT_URI, "" /* pszBasePath */, "\n", &pszBuf, &cbBuf), VINF_SUCCESS); + RTTestPrintf(hTest, RTTESTLVL_DEBUG, "Roots (URI):\n%s\n", pszBuf); + RTStrFree(pszBuf); + RTTEST_CHECK_RC(hTest, DnDTransferListGetRootsEx(&list, DNDTRANSFERLISTFMT_URI, "\\new\\base\\path", "\n", &pszBuf, &cbBuf), VINF_SUCCESS); + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, "Roots (URI, new base):\n%s\n", pszBuf); + RTStrFree(pszBuf); + RTTEST_CHECK_RC(hTest, DnDTransferListGetRootsEx(&list, DNDTRANSFERLISTFMT_URI, "\\..\\invalid\\path", "\n", &pszBuf, &cbBuf), VERR_INVALID_PARAMETER); + DnDTransferListDestroy(&list); + + /* From URI data. */ +#if RTPATH_STYLE == RTPATH_STR_F_STYLE_DOS + RTStrPrintf(szPathTest, sizeof(szPathTest), "C:/Windows/"); + static const char s_szURI[] = + "file:///C:/Windows/System32/Boot/\r\n" + "file:///C:/Windows/System/\r\n"; + static const char s_szURIFmtURI[] = + "file:///base/System32/Boot/\r\n" + "file:///base/System/\r\n"; + static const char s_szURIFmtNative[] = + "\\base\\System32\\Boot\\\r\n" + "\\base\\System\\\r\n"; +#else + RTStrPrintf(szPathTest, sizeof(szPathTest), "/usr/"); + static const char s_szURI[] = + "file:///usr/bin/\r\n" + "file:///usr/lib/\r\n"; + static const char s_szURIFmtURI[] = + "file:///base/bin/\r\n" + "file:///base/lib/\r\n"; + static const char s_szURIFmtNative[] = + "/base/bin/\r\n" + "/base/lib/\r\n"; +#endif + + RTTEST_CHECK_RC(hTest, DnDTransferListAppendPathsFromBuffer(&list, DNDTRANSFERLISTFMT_URI, s_szURI, sizeof(s_szURI), "\r\n", + DNDTRANSFERLIST_FLAGS_NONE), VINF_SUCCESS); + RTTEST_CHECK(hTest, DnDTransferListGetRootCount(&list) == 2); + RTTEST_CHECK(hTest, RTPathCompare(DnDTransferListGetRootPathAbs(&list), szPathTest) == 0); + + /* Validate returned lengths. */ + pszBuf = NULL; + RTTEST_CHECK_RC(hTest, DnDTransferListGetRootsEx(&list, DNDTRANSFERLISTFMT_URI, "/base/", "\r\n", &pszBuf, &cbBuf), VINF_SUCCESS); + RTTEST_CHECK_MSG(hTest, RTStrCmp(pszBuf, s_szURIFmtURI) == 0, (hTest, "Got '%s'", pszBuf)); + RTTEST_CHECK_MSG(hTest, cbBuf == strlen(pszBuf) + 1, (hTest, "Got %d, expected %d\n", cbBuf, strlen(pszBuf) + 1)); + RTStrFree(pszBuf); + + pszBuf = NULL; + RTTEST_CHECK_RC(hTest, DnDTransferListGetRootsEx(&list, DNDTRANSFERLISTFMT_NATIVE, "/base/", "\r\n", &pszBuf, &cbBuf), VINF_SUCCESS); + RTTEST_CHECK_MSG(hTest, RTStrCmp(pszBuf, s_szURIFmtNative) == 0, + (hTest, "Expected %.*Rhxs\nGot %.*Rhxs\n '%s'", + sizeof(s_szURIFmtNative) - 1, s_szURIFmtNative, + strlen(pszBuf), pszBuf, pszBuf)); + RTTEST_CHECK_MSG(hTest, cbBuf == strlen(pszBuf) + 1, (hTest, "Got %d, expected %d\n", cbBuf, strlen(pszBuf) + 1)); + RTStrFree(pszBuf); + + /* Validate roots with a new base. */ + pszBuf = NULL; + RTTEST_CHECK_RC(hTest, DnDTransferListGetRootsEx(&list, DNDTRANSFERLISTFMT_NATIVE, "/native/base/path", "\n", &pszBuf, &cbBuf), VINF_SUCCESS); + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, "Roots (URI, new base):\n%s\n", pszBuf); + RTStrFree(pszBuf); + + pszBuf = NULL; + RTTEST_CHECK_RC(hTest, DnDTransferListGetRootsEx(&list, DNDTRANSFERLISTFMT_NATIVE, "\\windows\\path", "\n", &pszBuf, &cbBuf), VINF_SUCCESS); + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, "Roots (URI, new base):\n%s\n", pszBuf); + RTStrFree(pszBuf); + + pszBuf = NULL; + RTTEST_CHECK_RC(hTest, DnDTransferListGetRootsEx(&list, DNDTRANSFERLISTFMT_NATIVE, "\\\\windows\\\\path", "\n", &pszBuf, &cbBuf), VINF_SUCCESS); + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, "Roots (URI, new base):\n%s\n", pszBuf); + RTStrFree(pszBuf); + + DnDTransferListDestroy(&list); + DnDTransferListDestroy(&list); /* Doing this twice here is intentional. */ + + return RTTestSummaryAndDestroy(hTest); +} + diff --git a/src/VBox/GuestHost/DragAndDrop/testcase/tstDnDTransferObject.cpp b/src/VBox/GuestHost/DragAndDrop/testcase/tstDnDTransferObject.cpp new file mode 100644 index 00000000..f887aac3 --- /dev/null +++ b/src/VBox/GuestHost/DragAndDrop/testcase/tstDnDTransferObject.cpp @@ -0,0 +1,128 @@ +/* $Id: tstDnDTransferObject.cpp $ */ +/** @file + * DnD URI object (DNDTRANSFEROBJECT) tests. + */ + +/* + * Copyright (C) 2020-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include +#include +#include +#include +#include +#include + +#include + + +static void tstPaths(RTTEST hTest) +{ + RTTestSub(hTest, "Testing path handling"); + + char szBuf[64]; + + DNDTRANSFEROBJECT Obj; + RT_ZERO(Obj); + + /* + * Initialization handling. + */ + RTTEST_CHECK(hTest, DnDTransferObjectGetSourcePath(&Obj) == NULL); + RTTEST_CHECK_RC(hTest, DnDTransferObjectGetDestPathEx(&Obj, DNDTRANSFEROBJPATHSTYLE_TRANSPORT, szBuf, sizeof(szBuf)), VERR_NOT_FOUND); + RTTEST_CHECK(hTest, DnDTransferObjectGetMode(&Obj) == 0); + RTTEST_CHECK(hTest, DnDTransferObjectGetSize(&Obj) == 0); + RTTEST_CHECK(hTest, DnDTransferObjectGetProcessed(&Obj) == 0); + RTTEST_CHECK(hTest, DnDTransferObjectGetType(&Obj) == DNDTRANSFEROBJTYPE_UNKNOWN); + + /* + * Paths handling. + */ + RTTEST_CHECK_RC_OK(hTest, DnDTransferObjectInitEx(&Obj, DNDTRANSFEROBJTYPE_FILE, "", "/rel/path/to/dst")); + RTTestDisableAssertions(hTest); + RTTEST_CHECK_RC (hTest, DnDTransferObjectInitEx(&Obj, DNDTRANSFEROBJTYPE_FILE, "", "/rel/path/to/dst"), VERR_WRONG_ORDER); + RTTestRestoreAssertions(hTest); + DnDTransferObjectReset(&Obj); + + RTTEST_CHECK_RC_OK(hTest, DnDTransferObjectInitEx(&Obj, DNDTRANSFEROBJTYPE_FILE, "/src/path1", "dst/path2")); + RTTEST_CHECK(hTest, RTStrCmp(DnDTransferObjectGetSourcePath(&Obj), "/src/path1/dst/path2") == 0); + RTTEST_CHECK(hTest, RTStrCmp(DnDTransferObjectGetDestPath(&Obj), "dst/path2") == 0); + RTTEST_CHECK(hTest, DnDTransferObjectGetDestPathEx(&Obj, DNDTRANSFEROBJPATHSTYLE_DOS, szBuf, sizeof(szBuf)) == VINF_SUCCESS + && RTStrCmp(szBuf, "dst\\path2") == 0); + DnDTransferObjectReset(&Obj); + RTTEST_CHECK_RC_OK(hTest, DnDTransferObjectInitEx(&Obj, DNDTRANSFEROBJTYPE_FILE, "", "dst/with/ending/slash/")); + RTTEST_CHECK(hTest, RTStrCmp(DnDTransferObjectGetDestPath(&Obj), "dst/with/ending/slash/") == 0); + RTTEST_CHECK(hTest, DnDTransferObjectGetDestPathEx(&Obj, DNDTRANSFEROBJPATHSTYLE_TRANSPORT, szBuf, sizeof(szBuf)) == VINF_SUCCESS + && RTStrCmp(szBuf, "dst/with/ending/slash/") == 0); + DnDTransferObjectReset(&Obj); + RTTEST_CHECK_RC_OK(hTest, DnDTransferObjectInitEx(&Obj, DNDTRANSFEROBJTYPE_DIRECTORY, "", "dst/path2")); + RTTEST_CHECK(hTest, RTStrCmp(DnDTransferObjectGetSourcePath(&Obj), "dst/path2/") == 0); + RTTEST_CHECK(hTest, RTStrCmp(DnDTransferObjectGetDestPath(&Obj), "dst/path2/") == 0); + DnDTransferObjectReset(&Obj); + RTTEST_CHECK_RC_OK(hTest, DnDTransferObjectInitEx(&Obj, DNDTRANSFEROBJTYPE_DIRECTORY, "", "dst\\to\\path2")); + RTTEST_CHECK(hTest, RTStrCmp(DnDTransferObjectGetSourcePath(&Obj), "dst/to/path2/") == 0); + RTTEST_CHECK(hTest, RTStrCmp(DnDTransferObjectGetDestPath(&Obj), "dst/to/path2/") == 0); + DnDTransferObjectReset(&Obj); + /* Test that the destination does not have a beginning slash. */ + RTTEST_CHECK_RC_OK(hTest, DnDTransferObjectInitEx(&Obj, DNDTRANSFEROBJTYPE_DIRECTORY, "/src/path2", "/dst/to/path2/")); + RTTEST_CHECK(hTest, RTStrCmp(DnDTransferObjectGetSourcePath(&Obj), "/src/path2/dst/to/path2/") == 0); + RTTEST_CHECK(hTest, RTStrCmp(DnDTransferObjectGetDestPath(&Obj), "dst/to/path2/") == 0); + DnDTransferObjectReset(&Obj); + RTTEST_CHECK_RC_OK(hTest, DnDTransferObjectInitEx(&Obj, DNDTRANSFEROBJTYPE_DIRECTORY, "/src/path2", "//////dst/to/path2/")); + RTTEST_CHECK(hTest, RTStrCmp(DnDTransferObjectGetDestPath(&Obj), "dst/to/path2/") == 0); + + /* + * Invalid stuff. + */ + DnDTransferObjectReset(&Obj); + RTTestDisableAssertions(hTest); + RTTEST_CHECK(hTest, DnDTransferObjectInitEx(&Obj, DNDTRANSFEROBJTYPE_DIRECTORY, "/src/path3", "../../dst/path3") == VERR_INVALID_PARAMETER); + RTTEST_CHECK(hTest, DnDTransferObjectInitEx(&Obj, DNDTRANSFEROBJTYPE_DIRECTORY, "/src/../../path3", "dst/path3") == VERR_INVALID_PARAMETER); + RTTestRestoreAssertions(hTest); + + /* + * Reset handling. + */ + DnDTransferObjectReset(&Obj); + RTTEST_CHECK(hTest, DnDTransferObjectGetSourcePath(&Obj) == NULL); + RTTEST_CHECK(hTest, DnDTransferObjectGetDestPath(&Obj) == NULL); + + DnDTransferObjectDestroy(&Obj); + DnDTransferObjectDestroy(&Obj); /* Doing this twice here is intentional. */ +} + +int main() +{ + /* + * Init the runtime, test and say hello. + */ + RTTEST hTest; + int rc = RTTestInitAndCreate("tstDnDTransferObject", &hTest); + if (rc) + return rc; + RTTestBanner(hTest); + + tstPaths(hTest); + + return RTTestSummaryAndDestroy(hTest); +} + diff --git a/src/VBox/GuestHost/DragAndDrop/testcase/tstDragAndDropQt/Makefile.kmk b/src/VBox/GuestHost/DragAndDrop/testcase/tstDragAndDropQt/Makefile.kmk new file mode 100644 index 00000000..4e3d8795 --- /dev/null +++ b/src/VBox/GuestHost/DragAndDrop/testcase/tstDragAndDropQt/Makefile.kmk @@ -0,0 +1,53 @@ +# $Id: Makefile.kmk $ +## @file +# Sub-Makefile for tstDragAndDropQt. +# + +# +# Copyright (C) 2022-2023 Oracle and/or its affiliates. +# +# This file is part of VirtualBox base platform packages, as +# available from https://www.virtualbox.org. +# +# 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, in version 3 of the +# License. +# +# 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, see . +# +# SPDX-License-Identifier: GPL-3.0-only +# + +SUB_DEPTH = ../../../../../.. +include $(KBUILD_PATH)/subheader.kmk + +# +# Targets and units. +# +USES += qt5 +PROGRAMS.linux += tstDragAndDropQt # Just needed for Linux right now. + +# +# tstDragAndDropQt +# +tstDragAndDropQt_TEMPLATE = VBoxQtGuiExe +tstDragAndDropQt_DEFS = UNIT_TEST TESTCASE +tstDragAndDropQt_NAME = tstDragAndDropQt +tstDragAndDropQt_CXXFLAGS = \ + $(VBOX_GCC_Wno-implicit-fallthrough) \ + $(VBOX_GCC_Wno-deprecated-declarations) + +tstDragAndDropQt_SOURCES = \ + tstDragAndDropQt.cpp + +# The Qt modules we're using. +tstDragAndDropQt_QT_MODULES = Core Gui Widgets PrintSupport + +include $(FILE_KBUILD_SUB_FOOTER) diff --git a/src/VBox/GuestHost/DragAndDrop/testcase/tstDragAndDropQt/tstDragAndDropQt.cpp b/src/VBox/GuestHost/DragAndDrop/testcase/tstDragAndDropQt/tstDragAndDropQt.cpp new file mode 100644 index 00000000..0d7a6144 --- /dev/null +++ b/src/VBox/GuestHost/DragAndDrop/testcase/tstDragAndDropQt/tstDragAndDropQt.cpp @@ -0,0 +1,67 @@ +/* $Id: tstDragAndDropQt.cpp $ */ +/** @file + * Drag and drop Qt code test cases. + */ + +/* + * Copyright (C) 2022-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include + +class DnDWin : public QWidget +{ + public: + + explicit DnDWin(QWidget *parent = nullptr) : QWidget(parent) + { + setMinimumSize(400, 400); + setWindowTitle("Drag me!"); + setAcceptDrops(true); + } + + void mouseMoveEvent(QMouseEvent *event) + { + if(!(event->buttons() & Qt::LeftButton)) + return DnDWin::mouseMoveEvent(event); + + event->accept(); + + QDrag *drag = new QDrag(this); + + QMimeData *mime = new QMimeData(); + mime->setData("text/plain", QString("/tmp/%1").arg("foo.bar").toLatin1()); + mime->setData("text/uri-list", QString("file:///tmp/%1").arg("foo.bar").toLatin1()); + + drag->setMimeData(mime); + drag->exec(); + } +}; + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + DnDWin win; + win.show(); + + app.exec(); +} diff --git a/src/VBox/GuestHost/HGSMI/.scm-settings b/src/VBox/GuestHost/HGSMI/.scm-settings new file mode 100644 index 00000000..e8698ae0 --- /dev/null +++ b/src/VBox/GuestHost/HGSMI/.scm-settings @@ -0,0 +1,31 @@ +# $Id: .scm-settings $ +## @file +# Source code massager settings for HGSMI. +# + +# +# Copyright (C) 2010-2023 Oracle and/or its affiliates. +# +# This file is part of VirtualBox base platform packages, as +# available from https://www.virtualbox.org. +# +# 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, in version 3 of the +# License. +# +# 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, see . +# +# SPDX-License-Identifier: GPL-3.0-only +# + +# @bugref{8524} r114561: MIT license of files upstreamed to the Linux kernel to +# encourage other kernels to pick them up. +HGSMICommon.cpp: --license-mit + diff --git a/src/VBox/GuestHost/HGSMI/HGSMICommon.cpp b/src/VBox/GuestHost/HGSMI/HGSMICommon.cpp new file mode 100644 index 00000000..31b8eba5 --- /dev/null +++ b/src/VBox/GuestHost/HGSMI/HGSMICommon.cpp @@ -0,0 +1,450 @@ +/* $Id: HGSMICommon.cpp $ */ +/** @file + * VBox Host Guest Shared Memory Interface (HGSMI) - Functions common to both host and guest. + */ + +/* + * Copyright (C) 2006-2023 Oracle and/or its affiliates. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#define LOG_DISABLED /* Maybe we can enabled it all the time now? */ +/** @note commented out all logging statements to avoid pulling the logging + * sub-system into places like the Linux kernel driver. Perhaps the best + * thing would be to use return enough information for callers to log what + * is needed. */ +#define LOG_GROUP LOG_GROUP_HGSMI + +#include + +#include +// #include + + +/* Channel flags. */ +#define HGSMI_CH_F_REGISTERED 0x01 + +/* Assertions for situations which could happen and normally must be processed properly + * but must be investigated during development: guest misbehaving, etc. + */ +#ifdef HGSMI_STRICT +#define HGSMI_STRICT_ASSERT_FAILED() AssertFailed() +#define HGSMI_STRICT_ASSERT(expr) Assert(expr) +#else +#define HGSMI_STRICT_ASSERT_FAILED() do {} while (0) +#define HGSMI_STRICT_ASSERT(expr) do {} while (0) +#endif /* !HGSMI_STRICT */ + +/* + * We do not want assertions in Linux kernel code to reduce symbol dependencies. + */ +#if defined(IN_RING0) && defined(RT_OS_LINUX) +# define HGSMI_ASSERT_PTR_RETURN(a, b) if (!(a)) return (b) +#else +# define HGSMI_ASSERT_PTR_RETURN(a, b) if (!(a)) return (b) +#endif /* !IN_RING0 && RT_OS_LINUX */ + +/* One-at-a-Time Hash from + * http://www.burtleburtle.net/bob/hash/doobs.html + * + * ub4 one_at_a_time(char *key, ub4 len) + * { + * ub4 hash, i; + * for (hash=0, i=0; i> 6); + * } + * hash += (hash << 3); + * hash ^= (hash >> 11); + * hash += (hash << 15); + * return hash; + * } + */ + +static uint32_t hgsmiHashBegin(void) +{ + return 0; +} + +static uint32_t hgsmiHashProcess(uint32_t hash, const void RT_UNTRUSTED_VOLATILE_HSTGST *pvData, size_t cbData) +{ + const uint8_t *pu8Data = (const uint8_t *)pvData; + + while (cbData--) + { + hash += *pu8Data++; + hash += (hash << 10); + hash ^= (hash >> 6); + } + + return hash; +} + +static uint32_t hgsmiHashEnd(uint32_t hash) +{ + hash += (hash << 3); + hash ^= (hash >> 11); + hash += (hash << 15); + + return hash; +} + +uint32_t HGSMIChecksum(HGSMIOFFSET offBuffer, const HGSMIBUFFERHEADER RT_UNTRUSTED_VOLATILE_HSTGST *pHeader, + const HGSMIBUFFERTAIL RT_UNTRUSTED_VOLATILE_HSTGST *pTail) +{ + uint32_t u32Checksum = hgsmiHashBegin(); + + u32Checksum = hgsmiHashProcess(u32Checksum, &offBuffer, sizeof(offBuffer)); + u32Checksum = hgsmiHashProcess(u32Checksum, pHeader, sizeof(HGSMIBUFFERHEADER)); + u32Checksum = hgsmiHashProcess(u32Checksum, pTail, RT_UOFFSETOF(HGSMIBUFFERTAIL, u32Checksum)); + + return hgsmiHashEnd(u32Checksum); +} + +int HGSMIAreaInitialize(HGSMIAREA *pArea, void *pvBase, HGSMISIZE cbArea, HGSMIOFFSET offBase) +{ + uint8_t *pu8Base = (uint8_t *)pvBase; + + if ( !pArea /* Check that the area: */ + || cbArea < HGSMIBufferMinimumSize() /* large enough; */ + || pu8Base + cbArea < pu8Base /* no address space wrap; */ + || offBase > UINT32_C(0xFFFFFFFF) - cbArea /* area within the 32 bit space: offBase + cbMem <= 0xFFFFFFFF. */ + ) + { + return VERR_INVALID_PARAMETER; + } + + pArea->pu8Base = pu8Base; + pArea->offBase = offBase; + pArea->offLast = cbArea - HGSMIBufferMinimumSize() + offBase; + pArea->cbArea = cbArea; + + return VINF_SUCCESS; +} + +void HGSMIAreaClear(HGSMIAREA *pArea) +{ + if (pArea) + { + RT_ZERO(*pArea); + } +} + +/* Initialize the memory buffer including its checksum. + * No changes alloed to the header and the tail after that. + */ +HGSMIOFFSET HGSMIBufferInitializeSingle(const HGSMIAREA *pArea, + HGSMIBUFFERHEADER *pHeader, + HGSMISIZE cbBuffer, + uint8_t u8Channel, + uint16_t u16ChannelInfo) +{ + if ( !pArea + || !pHeader + || cbBuffer < HGSMIBufferMinimumSize()) + { + return HGSMIOFFSET_VOID; + } + + /* Buffer must be within the area: + * * header data size do not exceed the maximum data size; + * * buffer address is greater than the area base address; + * * buffer address is lower than the maximum allowed for the given data size. + */ + HGSMISIZE cbMaximumDataSize = pArea->offLast - pArea->offBase; + uint32_t u32DataSize = cbBuffer - HGSMIBufferMinimumSize(); + + if ( u32DataSize > cbMaximumDataSize + || (uint8_t *)pHeader < pArea->pu8Base + || (uint8_t *)pHeader > pArea->pu8Base + cbMaximumDataSize - u32DataSize) + { + return HGSMIOFFSET_VOID; + } + + HGSMIOFFSET offBuffer = HGSMIPointerToOffset(pArea, pHeader); + + pHeader->u8Flags = HGSMI_BUFFER_HEADER_F_SEQ_SINGLE; + pHeader->u32DataSize = u32DataSize; + pHeader->u8Channel = u8Channel; + pHeader->u16ChannelInfo = u16ChannelInfo; + RT_ZERO(pHeader->u.au8Union); + + HGSMIBUFFERTAIL RT_UNTRUSTED_VOLATILE_HSTGST *pTail = HGSMIBufferTailFromPtr(pHeader, u32DataSize); + pTail->u32Reserved = 0; + pTail->u32Checksum = HGSMIChecksum(offBuffer, pHeader, pTail); + + return offBuffer; +} + +int HGSMIHeapSetup(HGSMIHEAP *pHeap, void *pvBase, HGSMISIZE cbArea, HGSMIOFFSET offBase, const HGSMIENV *pEnv) +{ + HGSMI_ASSERT_PTR_RETURN(pHeap, VERR_INVALID_PARAMETER); + HGSMI_ASSERT_PTR_RETURN(pvBase, VERR_INVALID_PARAMETER); + + int rc = HGSMIAreaInitialize(&pHeap->area, pvBase, cbArea, offBase); + if (RT_SUCCESS(rc)) + { + rc = HGSMIMAInit(&pHeap->ma, &pHeap->area, NULL, 0, 0, pEnv); + if (RT_FAILURE(rc)) + { + HGSMIAreaClear(&pHeap->area); + } + } + + return rc; +} + +void HGSMIHeapDestroy(HGSMIHEAP *pHeap) +{ + if (pHeap) + { + HGSMIMAUninit(&pHeap->ma); + RT_ZERO(*pHeap); + } +} + +void RT_UNTRUSTED_VOLATILE_HOST *HGSMIHeapAlloc(HGSMIHEAP *pHeap, + HGSMISIZE cbData, + uint8_t u8Channel, + uint16_t u16ChannelInfo) +{ + HGSMISIZE cbAlloc = HGSMIBufferRequiredSize(cbData); + HGSMIBUFFERHEADER *pHeader = (HGSMIBUFFERHEADER *)HGSMIHeapBufferAlloc(pHeap, cbAlloc); + if (pHeader) + { + HGSMIOFFSET offBuffer = HGSMIBufferInitializeSingle(HGSMIHeapArea(pHeap), pHeader, + cbAlloc, u8Channel, u16ChannelInfo); + if (offBuffer == HGSMIOFFSET_VOID) + { + HGSMIHeapBufferFree(pHeap, pHeader); + pHeader = NULL; + } + } + + return pHeader? HGSMIBufferDataFromPtr(pHeader): NULL; +} + +void HGSMIHeapFree(HGSMIHEAP *pHeap, void RT_UNTRUSTED_VOLATILE_GUEST *pvData) +{ + if (pvData) + { + HGSMIBUFFERHEADER RT_UNTRUSTED_VOLATILE_HOST *pHeader = HGSMIBufferHeaderFromData(pvData); + HGSMIHeapBufferFree(pHeap, pHeader); + } +} + +void RT_UNTRUSTED_VOLATILE_HSTGST *HGSMIHeapBufferAlloc(HGSMIHEAP *pHeap, HGSMISIZE cbBuffer) +{ + return HGSMIMAAlloc(&pHeap->ma, cbBuffer); +} + +void HGSMIHeapBufferFree(HGSMIHEAP *pHeap, void RT_UNTRUSTED_VOLATILE_GUEST *pvBuf) +{ + HGSMIMAFree(&pHeap->ma, pvBuf); +} + +typedef struct HGSMIBUFFERCONTEXT +{ + /** The original buffer header. */ + const HGSMIBUFFERHEADER RT_UNTRUSTED_VOLATILE_HSTGST *pHeader; + /** Payload data in the buffer. */ + void RT_UNTRUSTED_VOLATILE_HSTGST *pvData; + /** Size of data */ + uint32_t cbData; +} HGSMIBUFFERCONTEXT; + +/** Verify that the given offBuffer points to a valid buffer, which is within the area. + * + * @returns VBox status and the buffer information in pBufferContext. + * @param pArea Area which supposed to contain the buffer. + * @param offBuffer The buffer location in the area. + * @param pBufferContext Where to write information about the buffer. + */ +static int hgsmiVerifyBuffer(const HGSMIAREA *pArea, HGSMIOFFSET offBuffer, HGSMIBUFFERCONTEXT *pBufferContext) +{ + // LogFlowFunc(("buffer 0x%x, area %p %x [0x%x;0x%x]\n", + // offBuffer, pArea->pu8Base, pArea->cbArea, pArea->offBase, pArea->offLast)); + + int rc = VINF_SUCCESS; + + if ( offBuffer < pArea->offBase + || offBuffer > pArea->offLast) + { + // LogFunc(("offset 0x%x is outside the area [0x%x;0x%x]!!!\n", + // offBuffer, pArea->offBase, pArea->offLast)); + rc = VERR_INVALID_PARAMETER; + HGSMI_STRICT_ASSERT_FAILED(); + } + else + { + void RT_UNTRUSTED_VOLATILE_HSTGST *pvBuffer = HGSMIOffsetToPointer(pArea, offBuffer); + HGSMIBUFFERHEADER header; + memcpy(&header, (void *)HGSMIBufferHeaderFromPtr(pvBuffer), sizeof(header)); + ASMCompilerBarrier(); + + /* Quick check of the data size, it should be less than the maximum + * data size for the buffer at this offset. + */ + // LogFlowFunc(("datasize check: header.u32DataSize = 0x%x pArea->offLast - offBuffer = 0x%x\n", + // header.u32DataSize, pArea->offLast - offBuffer)); + + if (header.u32DataSize <= pArea->offLast - offBuffer) + { + HGSMIBUFFERTAIL tail; + memcpy(&tail, (void *)HGSMIBufferTailFromPtr(pvBuffer, header.u32DataSize), sizeof(tail)); + ASMCompilerBarrier(); + + /* At least both header and tail structures are in the area. Check the checksum. */ + uint32_t u32Checksum = HGSMIChecksum(offBuffer, &header, &tail); + // LogFlowFunc(("checksum check: u32Checksum = 0x%x pTail->u32Checksum = 0x%x\n", + // u32Checksum, tail.u32Checksum)); + if (u32Checksum == tail.u32Checksum) + { + /* Success. */ + pBufferContext->pHeader = HGSMIBufferHeaderFromPtr(pvBuffer); + pBufferContext->pvData = HGSMIBufferDataFromPtr(pvBuffer); + pBufferContext->cbData = header.u32DataSize; + } + else + { + // LogFunc(("invalid checksum 0x%x, expected 0x%x!!!\n", + // u32Checksum, tail.u32Checksum)); + rc = VERR_INVALID_STATE; + HGSMI_STRICT_ASSERT_FAILED(); + } + } + else + { + // LogFunc(("invalid data size 0x%x, maximum is 0x%x!!!\n", + // header.u32DataSize, pArea->offLast - offBuffer)); + rc = VERR_TOO_MUCH_DATA; + HGSMI_STRICT_ASSERT_FAILED(); + } + } + + return rc; +} + +/** Helper to convert HGSMI channel index to the channel structure pointer. + * + * @returns Pointer to the channel data. + * @param pChannelInfo The channel pool. + * @param u8Channel The channel index. + */ +HGSMICHANNEL *HGSMIChannelFindById(HGSMICHANNELINFO *pChannelInfo, + uint8_t u8Channel) +{ + AssertCompile(RT_ELEMENTS(pChannelInfo->Channels) >= 0x100); + HGSMICHANNEL *pChannel = &pChannelInfo->Channels[u8Channel]; + + if (pChannel->u8Flags & HGSMI_CH_F_REGISTERED) + { + return pChannel; + } + + return NULL; +} + +/** Process a guest buffer. + * + * @returns VBox status code. + * @param pArea Area which supposed to contain the buffer. + * @param pChannelInfo The channel pool. + * @param offBuffer The buffer location in the area. + */ +int HGSMIBufferProcess(const HGSMIAREA *pArea, + HGSMICHANNELINFO *pChannelInfo, + HGSMIOFFSET offBuffer) +{ + // LogFlowFunc(("pArea %p, offBuffer 0x%x\n", pArea, offBuffer)); + + HGSMI_ASSERT_PTR_RETURN(pArea, VERR_INVALID_PARAMETER); + HGSMI_ASSERT_PTR_RETURN(pChannelInfo, VERR_INVALID_PARAMETER); + + /* Guest has prepared a command description at 'offBuffer'. */ + HGSMIBUFFERCONTEXT bufferContext = { NULL, NULL, 0 }; /* Makes old GCC happier. */ + int rc = hgsmiVerifyBuffer(pArea, offBuffer, &bufferContext); + if (RT_SUCCESS(rc)) + { + /* Pass the command to the appropriate handler registered with this instance. + * Start with the handler list head, which is the preallocated HGSMI setup channel. + */ + const HGSMICHANNEL *pChannel = HGSMIChannelFindById(pChannelInfo, bufferContext.pHeader->u8Channel); + if (pChannel) + { + const HGSMICHANNELHANDLER *pHandler = &pChannel->handler; + if (pHandler->pfnHandler) + { + pHandler->pfnHandler(pHandler->pvHandler, bufferContext.pHeader->u16ChannelInfo, + bufferContext.pvData, bufferContext.cbData); + } + HGSMI_STRICT_ASSERT(RT_SUCCESS(hgsmiVerifyBuffer(pArea, offBuffer, &bufferContext))); + } + else + { + rc = VERR_INVALID_FUNCTION; + HGSMI_STRICT_ASSERT_FAILED(); + } + } + + return rc; +} + +/** Register a new HGSMI channel by index. + * + * @returns VBox status code. + * @param pChannelInfo The channel pool managed by the caller. + * @param u8Channel Index of the channel. + * @param pszName Name of the channel (optional, allocated by the caller). + * @param pfnChannelHandler The channel callback. + * @param pvChannelHandler The callback pointer. + */ +int HGSMIChannelRegister(HGSMICHANNELINFO *pChannelInfo, + uint8_t u8Channel, + const char *pszName, + PFNHGSMICHANNELHANDLER pfnChannelHandler, + void *pvChannelHandler) +{ + /* Check whether the channel is already registered. */ + HGSMICHANNEL *pChannel = HGSMIChannelFindById(pChannelInfo, u8Channel); + if (pChannel) + { + HGSMI_STRICT_ASSERT_FAILED(); + return VERR_ALREADY_EXISTS; + } + + /* Channel is not yet registered. */ + pChannel = &pChannelInfo->Channels[u8Channel]; + + pChannel->u8Flags = HGSMI_CH_F_REGISTERED; + pChannel->u8Channel = u8Channel; + + pChannel->handler.pfnHandler = pfnChannelHandler; + pChannel->handler.pvHandler = pvChannelHandler; + + pChannel->pszName = pszName; + + return VINF_SUCCESS; +} diff --git a/src/VBox/GuestHost/HGSMI/HGSMIMemAlloc.cpp b/src/VBox/GuestHost/HGSMI/HGSMIMemAlloc.cpp new file mode 100644 index 00000000..d9210f70 --- /dev/null +++ b/src/VBox/GuestHost/HGSMI/HGSMIMemAlloc.cpp @@ -0,0 +1,684 @@ +/* $Id: HGSMIMemAlloc.cpp $ */ +/** @file + * VBox Host Guest Shared Memory Interface (HGSMI) - Memory allocator. + */ + +/* + * Copyright (C) 2014-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +/* + * Memory allocator + * ---------------- + * + * Area [0; AreaSize) contains only the data, control structures are separate. + * Block sizes are power of 2: 32B, ..., 1MB + * Area size can be anything and will be divided initially to largest possible free blocks. + * + * The entire area is described by a list of 32 bit block descriptors: + * * bits 0..3 - order, which is log2 size of the block - 5: 2^(0+5) ... 2^(15+5) == 32B .. 1MB + * * bit 4 - 1 for free blocks. + * * bits 5..31 - block offset. + * + * 31 ... 5 | 4 | 3 ... 0 + * offset F order + * + * There is a sorted collection of all block descriptors + * (key is the block offset, bits 0...4 do not interfere with sorting). + * Also there are lists of free blocks for each size for fast allocation. + * + * + * Implementation + * -------------- + * + * The blocks collection is a sorted linear list. + * + * Initially the entire area consists of one or more largest blocks followed by smaller blocks: + * * 100B area - 64B block with descriptor: 0x00000011 + * 32B block with descriptor: 0x00000030 + * 4B unused + * * 64K area - one 64K block with descriptor: 0x0000001C + * * 512K area - one 512K block with descriptor: 0x0000001F + * + * When allocating a new block: + * * larger free blocks are splitted when there are no smaller free blocks; + * * smaller free blocks are merged if they can build a requested larger block. + */ +#include +#include + +#include + +/* + * We do not want assertions in Linux kernel code to reduce symbol dependencies. + */ +#if defined(IN_RING0) && defined(RT_OS_LINUX) +# define HGSMI_ASSERT_RETURN(a, b) if (!(a)) return (b) +# define HGSMI_ASSERT_FAILED() do {} while (0) +# define HGSMI_ASSERT(expr) do {} while (0) +#else +# define HGSMI_ASSERT_RETURN(a, b) AssertReturn(a, b) +# define HGSMI_ASSERT_FAILED() AssertFailed() +# define HGSMI_ASSERT(expr) Assert(expr) +#endif /* !IN_RING0 && RT_OS_LINUX */ + +DECLINLINE(HGSMIOFFSET) hgsmiMADescriptor(HGSMIOFFSET off, bool fFree, HGSMIOFFSET order) +{ + return (off & HGSMI_MA_DESC_OFFSET_MASK) | + (fFree? HGSMI_MA_DESC_FREE_MASK: 0) | + (order & HGSMI_MA_DESC_ORDER_MASK); +} + +static void hgsmiMABlockFree(HGSMIMADATA *pMA, HGSMIMABLOCK *pBlock) +{ + pMA->env.pfnFree(pMA->env.pvEnv, pBlock); +} + +static int hgsmiMABlockAlloc(HGSMIMADATA *pMA, HGSMIMABLOCK **ppBlock) +{ + int rc = VINF_SUCCESS; + + HGSMIMABLOCK *pBlock = (HGSMIMABLOCK *)pMA->env.pfnAlloc(pMA->env.pvEnv, sizeof(HGSMIMABLOCK)); + if (pBlock) + { + RT_ZERO(pBlock->nodeBlock); + *ppBlock = pBlock; + } + else + { + rc = VERR_NO_MEMORY; + } + + return rc; +} + +/* Divide entire area to free blocks. */ +static int hgsmiMAFormat(HGSMIMADATA *pMA) +{ + int rc = VINF_SUCCESS; + + /* Initial value, it will be updated in the loop below. */ + pMA->cbMaxBlock = HGSMI_MA_BLOCK_SIZE_MIN; + pMA->cBlocks = 0; + + HGSMISIZE cbBlock = HGSMI_MA_BLOCK_SIZE_MAX; + HGSMISIZE cbRemaining = pMA->area.cbArea; + HGSMIOFFSET off = 0; + + while (cbBlock >= HGSMI_MA_BLOCK_SIZE_MIN) + { + /* Build a list of free memory blocks with u32BlockSize. */ + uint32_t cBlocks = cbRemaining / cbBlock; + if (cBlocks > 0) + { + if (pMA->cbMaxBlock < cbBlock) + { + pMA->cbMaxBlock = cbBlock; + } + + HGSMIOFFSET order = HGSMIMASize2Order(cbBlock); + + uint32_t i; + for (i = 0; i < cBlocks; ++i) + { + /* A new free block. */ + HGSMIMABLOCK *pBlock; + rc = hgsmiMABlockAlloc(pMA, &pBlock); + if (RT_FAILURE(rc)) + { + break; + } + + pBlock->descriptor = hgsmiMADescriptor(off, true, order); + RTListAppend(&pMA->listBlocks, &pBlock->nodeBlock); + ++pMA->cBlocks; + + off += cbBlock; + cbRemaining -= cbBlock; + } + } + + if (RT_FAILURE(rc)) + { + break; + } + + cbBlock /= 2; + } + + return rc; +} + +static int hgsmiMARebuildFreeLists(HGSMIMADATA *pMA) +{ + int rc = VINF_SUCCESS; + + HGSMIMABLOCK *pIter; + RTListForEach(&pMA->listBlocks, pIter, HGSMIMABLOCK, nodeBlock) + { + if (HGSMI_MA_DESC_IS_FREE(pIter->descriptor)) + { + HGSMIOFFSET order = HGSMI_MA_DESC_ORDER(pIter->descriptor); + RTListAppend(&pMA->aListFreeBlocks[order], &pIter->nodeFree); + } + } + + return rc; +} + +static int hgsmiMARestore(HGSMIMADATA *pMA, HGSMIOFFSET *paDescriptors, uint32_t cDescriptors, HGSMISIZE cbMaxBlock) +{ + int rc = VINF_SUCCESS; + + pMA->cbMaxBlock = cbMaxBlock; + pMA->cBlocks = 0; + + HGSMISIZE cbRemaining = pMA->area.cbArea; + HGSMIOFFSET off = 0; + + uint32_t i; + for (i = 0; i < cDescriptors; ++i) + { + /* Verify the descriptor. */ + HGSMISIZE cbBlock = HGSMIMAOrder2Size(HGSMI_MA_DESC_ORDER(paDescriptors[i])); + if ( off != HGSMI_MA_DESC_OFFSET(paDescriptors[i]) + || cbBlock > cbRemaining + || cbBlock > pMA->cbMaxBlock) + { + rc = VERR_INVALID_PARAMETER; + break; + } + + /* A new free block. */ + HGSMIMABLOCK *pBlock; + rc = hgsmiMABlockAlloc(pMA, &pBlock); + if (RT_FAILURE(rc)) + { + break; + } + + pBlock->descriptor = paDescriptors[i]; + RTListAppend(&pMA->listBlocks, &pBlock->nodeBlock); + ++pMA->cBlocks; + + off += cbBlock; + cbRemaining -= cbBlock; + } + + return rc; +} + +static HGSMIMABLOCK *hgsmiMAGetFreeBlock(HGSMIMADATA *pMA, HGSMIOFFSET order) +{ + HGSMIMABLOCK *pBlock = NULL; + + HGSMIOFFSET i; + for (i = order; i < RT_ELEMENTS(pMA->aListFreeBlocks); ++i) + { + pBlock = RTListGetFirst(&pMA->aListFreeBlocks[i], HGSMIMABLOCK, nodeFree); + if (pBlock) + { + break; + } + } + + if (pBlock) + { + HGSMI_ASSERT_RETURN(HGSMI_MA_DESC_IS_FREE(pBlock->descriptor), NULL); + + /* Where the block starts. */ + HGSMIOFFSET off = HGSMI_MA_DESC_OFFSET(pBlock->descriptor); + + /* 'i' is the order of the block. */ + while (i != order) + { + /* A larger block was found and need to be split to 2 smaller blocks. */ + HGSMIMABLOCK *pBlock2; + int rc = hgsmiMABlockAlloc(pMA, &pBlock2); + if (RT_FAILURE(rc)) + { + pBlock = NULL; + break; + } + + /* Create 2 blocks with descreased order. */ + --i; + + /* Remove from the free list. */ + RTListNodeRemove(&pBlock->nodeFree); + + pBlock->descriptor = hgsmiMADescriptor(off, true, i); + pBlock2->descriptor = hgsmiMADescriptor(off + HGSMIMAOrder2Size(i), true, i); + + /* Update list of all blocks by inserting pBlock2 after pBlock. */ + RTListNodeInsertAfter(&pBlock->nodeBlock, &pBlock2->nodeBlock); + ++pMA->cBlocks; + + /* Update the free list. */ + RTListAppend(&pMA->aListFreeBlocks[i], &pBlock->nodeFree); + RTListAppend(&pMA->aListFreeBlocks[i], &pBlock2->nodeFree); + } + } + + return pBlock; +} + +static void hgsmiMAReformatFreeBlocks(HGSMIMADATA *pMA, HGSMIOFFSET maxId, + HGSMIMABLOCK *pStart, HGSMIMABLOCK *pEnd, HGSMISIZE cbBlocks) +{ + int rc = VINF_SUCCESS; + + /* + * Blocks starting from pStart until pEnd will be replaced with + * another set of blocks. + * + * The new set will include the block with the required order. + * Since the required order is larger than any existing block, + * it will replace at least two existing blocks. + * The new set will also have minimal possible number of blocks. + * Therefore the new set will have at least one block less. + * Blocks will be updated in place and remaining blocks will be + * deallocated. + */ + + HGSMISIZE u32BlockSize = HGSMIMAOrder2Size(maxId); + HGSMISIZE cbRemaining = cbBlocks; + HGSMIOFFSET off = HGSMI_MA_DESC_OFFSET(pStart->descriptor); + HGSMIMABLOCK *pBlock = pStart; + + while (u32BlockSize >= HGSMI_MA_BLOCK_SIZE_MIN && cbRemaining) + { + /* Build a list of free memory blocks with u32BlockSize. */ + uint32_t cBlocks = cbRemaining / u32BlockSize; + if (cBlocks > 0) + { + HGSMIOFFSET order = HGSMIMASize2Order(u32BlockSize); + + uint32_t i; + for (i = 0; i < cBlocks; ++i) + { + if (pBlock == pEnd) + { + /* Should never happen because the new set of blocks is supposed to be smaller. */ + HGSMI_ASSERT_FAILED(); + rc = VERR_OUT_OF_RESOURCES; + break; + } + + /* Remove from the free list. */ + RTListNodeRemove(&pBlock->nodeFree); + + pBlock->descriptor = hgsmiMADescriptor(off, true, order); + + RTListAppend(&pMA->aListFreeBlocks[order], &pBlock->nodeFree); + + off += u32BlockSize; + cbRemaining -= u32BlockSize; + + pBlock = RTListGetNext(&pMA->listBlocks, pBlock, HGSMIMABLOCK, nodeBlock); + } + } + + if (RT_FAILURE(rc)) + { + break; + } + + u32BlockSize /= 2; + } + + HGSMI_ASSERT(cbRemaining == 0); + + if (RT_SUCCESS(rc)) + { + /* Remove remaining free blocks from pBlock until pEnd */ + for (;;) + { + bool fEnd = (pBlock == pEnd); + HGSMIMABLOCK *pNext = RTListGetNext(&pMA->listBlocks, pBlock, HGSMIMABLOCK, nodeBlock); + + RTListNodeRemove(&pBlock->nodeFree); + RTListNodeRemove(&pBlock->nodeBlock); + --pMA->cBlocks; + + hgsmiMABlockFree(pMA, pBlock); + + if (fEnd) + { + break; + } + + pBlock = pNext; + } + } +} + +static void hgsmiMAQueryFreeRange(HGSMIMADATA *pMA, HGSMIMABLOCK *pBlock, HGSMISIZE cbRequired, + HGSMIMABLOCK **ppStart, HGSMIMABLOCK **ppEnd, HGSMISIZE *pcbBlocks) +{ + HGSMI_ASSERT(HGSMI_MA_DESC_IS_FREE(pBlock->descriptor)); + + *pcbBlocks = HGSMIMAOrder2Size(HGSMI_MA_DESC_ORDER(pBlock->descriptor)); + *ppStart = pBlock; + *ppEnd = pBlock; + + HGSMIMABLOCK *p; + for (;;) + { + p = RTListGetNext(&pMA->listBlocks, *ppEnd, HGSMIMABLOCK, nodeBlock); + if (!p || !HGSMI_MA_DESC_IS_FREE(p->descriptor)) + { + break; + } + *pcbBlocks += HGSMIMAOrder2Size(HGSMI_MA_DESC_ORDER(p->descriptor)); + *ppEnd = p; + + if (cbRequired && *pcbBlocks >= cbRequired) + { + return; + } + } + for (;;) + { + p = RTListGetPrev(&pMA->listBlocks, *ppStart, HGSMIMABLOCK, nodeBlock); + if (!p || !HGSMI_MA_DESC_IS_FREE(p->descriptor)) + { + break; + } + *pcbBlocks += HGSMIMAOrder2Size(HGSMI_MA_DESC_ORDER(p->descriptor)); + *ppStart = p; + + if (cbRequired && *pcbBlocks >= cbRequired) + { + return; + } + } +} + +static void hgsmiMAMergeFreeBlocks(HGSMIMADATA *pMA, HGSMIOFFSET order) +{ + /* Try to create a free block with the order from smaller free blocks. */ + if (order == 0) + { + /* No smaller blocks. */ + return; + } + + HGSMISIZE cbRequired = HGSMIMAOrder2Size(order); + + /* Scan all free lists of smaller blocks. + * + * Get the sequence of free blocks before and after each free block. + * If possible, re-split the sequence to get the required block and other free block(s). + * If not possible, try the next free block. + * + * Free blocks are scanned from i to 0 orders. + */ + HGSMIOFFSET i = order - 1; + for (;;) + { + HGSMIMABLOCK *pIter; + RTListForEach(&pMA->aListFreeBlocks[i], pIter, HGSMIMABLOCK, nodeFree) + { + HGSMI_ASSERT(HGSMI_MA_DESC_ORDER(pIter->descriptor) == i); + + HGSMISIZE cbBlocks; + HGSMIMABLOCK *pFreeStart; + HGSMIMABLOCK *pFreeEnd; + hgsmiMAQueryFreeRange(pMA, pIter, cbRequired, &pFreeStart, &pFreeEnd, &cbBlocks); + + HGSMI_ASSERT((cbBlocks / HGSMI_MA_BLOCK_SIZE_MIN) * HGSMI_MA_BLOCK_SIZE_MIN == cbBlocks); + + /* Verify whether cbBlocks is enough for the requested block. */ + if (cbBlocks >= cbRequired) + { + /* Build new free blocks starting from the requested. */ + hgsmiMAReformatFreeBlocks(pMA, order, pFreeStart, pFreeEnd, cbBlocks); + i = 0; /* Leave the loop. */ + break; + } + } + + if (i == 0) + { + break; + } + + --i; + } +} + +static HGSMIOFFSET hgsmiMAAlloc(HGSMIMADATA *pMA, HGSMISIZE cb) +{ + if (cb > pMA->cbMaxBlock) + { + return HGSMIOFFSET_VOID; + } + + if (cb < HGSMI_MA_BLOCK_SIZE_MIN) + { + cb = HGSMI_MA_BLOCK_SIZE_MIN; + } + + HGSMIOFFSET order = HGSMIPopCnt32(cb - 1) - HGSMI_MA_DESC_ORDER_BASE; + + HGSMI_ASSERT_RETURN(HGSMIMAOrder2Size(order) >= cb, HGSMIOFFSET_VOID); + HGSMI_ASSERT_RETURN(order < RT_ELEMENTS(pMA->aListFreeBlocks), HGSMIOFFSET_VOID); + + HGSMIMABLOCK *pBlock = hgsmiMAGetFreeBlock(pMA, order); + if (RT_UNLIKELY(pBlock == NULL)) + { + /* No free block with large enough size. Merge smaller free blocks and try again. */ + hgsmiMAMergeFreeBlocks(pMA, order); + pBlock = hgsmiMAGetFreeBlock(pMA, order); + } + + if (RT_LIKELY(pBlock != NULL)) + { + RTListNodeRemove(&pBlock->nodeFree); + pBlock->descriptor &= ~HGSMI_MA_DESC_FREE_MASK; + return HGSMI_MA_DESC_OFFSET(pBlock->descriptor); + } + + return HGSMIOFFSET_VOID; +} + +static void hgsmiMAFree(HGSMIMADATA *pMA, HGSMIOFFSET off) +{ + if (off == HGSMIOFFSET_VOID) + { + return; + } + + /* Find the block corresponding to the offset. */ + HGSMI_ASSERT((off / HGSMI_MA_BLOCK_SIZE_MIN) * HGSMI_MA_BLOCK_SIZE_MIN == off); + + HGSMIMABLOCK *pBlock = HGSMIMASearchOffset(pMA, off); + if (pBlock) + { + if (HGSMI_MA_DESC_OFFSET(pBlock->descriptor) == off) + { + /* Found the right block, mark it as free. */ + pBlock->descriptor |= HGSMI_MA_DESC_FREE_MASK; + RTListAppend(&pMA->aListFreeBlocks[HGSMI_MA_DESC_ORDER(pBlock->descriptor)], &pBlock->nodeFree); + return; + } + } + + HGSMI_ASSERT_FAILED(); +} + +int HGSMIMAInit(HGSMIMADATA *pMA, const HGSMIAREA *pArea, + HGSMIOFFSET *paDescriptors, uint32_t cDescriptors, HGSMISIZE cbMaxBlock, + const HGSMIENV *pEnv) +{ + HGSMI_ASSERT_RETURN(pArea->cbArea < UINT32_C(0x80000000), VERR_INVALID_PARAMETER); + HGSMI_ASSERT_RETURN(pArea->cbArea >= HGSMI_MA_BLOCK_SIZE_MIN, VERR_INVALID_PARAMETER); + + RT_ZERO(*pMA); + + HGSMISIZE cb = (pArea->cbArea / HGSMI_MA_BLOCK_SIZE_MIN) * HGSMI_MA_BLOCK_SIZE_MIN; + + int rc = HGSMIAreaInitialize(&pMA->area, pArea->pu8Base, cb, 0); + if (RT_SUCCESS(rc)) + { + pMA->env = *pEnv; + + uint32_t i; + for (i = 0; i < RT_ELEMENTS(pMA->aListFreeBlocks); ++i) + { + RTListInit(&pMA->aListFreeBlocks[i]); + } + RTListInit(&pMA->listBlocks); + + if (cDescriptors) + { + rc = hgsmiMARestore(pMA, paDescriptors, cDescriptors, cbMaxBlock); + } + else + { + rc = hgsmiMAFormat(pMA); + } + + if (RT_SUCCESS(rc)) + { + rc = hgsmiMARebuildFreeLists(pMA); + } + } + + return rc; +} + +void HGSMIMAUninit(HGSMIMADATA *pMA) +{ + HGSMIMABLOCK *pIter; + HGSMIMABLOCK *pNext; + /* If it has been initialized. */ + if (pMA->listBlocks.pNext) + { + RTListForEachSafe(&pMA->listBlocks, pIter, pNext, HGSMIMABLOCK, nodeBlock) + { + RTListNodeRemove(&pIter->nodeBlock); + hgsmiMABlockFree(pMA, pIter); + } + } + + RT_ZERO(*pMA); +} + +HGSMIOFFSET HGSMIMAPointerToOffset(const HGSMIMADATA *pMA, const void RT_UNTRUSTED_VOLATILE_GUEST *pv) +{ + uintptr_t off = (uintptr_t)pv - (uintptr_t)pMA->area.pu8Base; + if (off < pMA->area.cbArea) + return pMA->area.offBase + off; + + HGSMI_ASSERT_FAILED(); + return HGSMIOFFSET_VOID; +} + +static void RT_UNTRUSTED_VOLATILE_HSTGST *HGSMIMAOffsetToPointer(const HGSMIMADATA *pMA, HGSMIOFFSET off) +{ + if (HGSMIAreaContainsOffset(&pMA->area, off)) + { + return HGSMIOffsetToPointer(&pMA->area, off); + } + + HGSMI_ASSERT_FAILED(); + return NULL; +} + +void RT_UNTRUSTED_VOLATILE_HSTGST *HGSMIMAAlloc(HGSMIMADATA *pMA, HGSMISIZE cb) +{ + HGSMIOFFSET off = hgsmiMAAlloc(pMA, cb); + return HGSMIMAOffsetToPointer(pMA, off); +} + +void HGSMIMAFree(HGSMIMADATA *pMA, void RT_UNTRUSTED_VOLATILE_GUEST *pv) +{ + HGSMIOFFSET off = HGSMIMAPointerToOffset(pMA, pv); + if (off != HGSMIOFFSET_VOID) + { + hgsmiMAFree(pMA, off); + } + else + { + HGSMI_ASSERT_FAILED(); + } +} + +HGSMIMABLOCK *HGSMIMASearchOffset(HGSMIMADATA *pMA, HGSMIOFFSET off) +{ + /* Binary search in the block list for the offset. */ + HGSMIMABLOCK *pStart = RTListGetFirst(&pMA->listBlocks, HGSMIMABLOCK, nodeBlock); + HGSMIMABLOCK *pEnd = RTListGetLast(&pMA->listBlocks, HGSMIMABLOCK, nodeBlock); + HGSMIMABLOCK *pMiddle; + + uint32_t iStart = 0; + uint32_t iEnd = pMA->cBlocks; + uint32_t iMiddle; + + for (;;) + { + pMiddle = pStart; + iMiddle = iStart + (iEnd - iStart) / 2; + if (iMiddle == iStart) + { + break; + } + + /* Find the block with the iMiddle index. Never go further than pEnd. */ + uint32_t i; + for (i = iStart; i < iMiddle && pMiddle != pEnd; ++i) + { + pMiddle = RTListNodeGetNext(&pMiddle->nodeBlock, HGSMIMABLOCK, nodeBlock); + } + + HGSMIOFFSET offMiddle = HGSMI_MA_DESC_OFFSET(pMiddle->descriptor); + if (offMiddle > off) + { + pEnd = pMiddle; + iEnd = iMiddle; + } + else + { + pStart = pMiddle; + iStart = iMiddle; + } + } + + return pMiddle; +} + + +/* + * Helper. + */ + +uint32_t HGSMIPopCnt32(uint32_t u32) +{ + uint32_t c = 0; + if (u32 > 0xFFFF) { c += 16; u32 >>= 16; } + if (u32 > 0xFF) { c += 8; u32 >>= 8; } + if (u32 > 0xF) { c += 4; u32 >>= 4; } + if (u32 > 0x3) { c += 2; u32 >>= 2; } + if (u32 > 0x1) { c += 1; u32 >>= 1; } + return c + u32; +} diff --git a/src/VBox/GuestHost/HGSMI/Makefile.kmk b/src/VBox/GuestHost/HGSMI/Makefile.kmk new file mode 100644 index 00000000..51453b07 --- /dev/null +++ b/src/VBox/GuestHost/HGSMI/Makefile.kmk @@ -0,0 +1,59 @@ +# $Id: Makefile.kmk $ +## @file +# Sub-Makefile for the common HGSMI library. +# + +# +# Copyright (C) 2006-2023 Oracle and/or its affiliates. +# +# This file is part of VirtualBox base platform packages, as +# available from https://www.virtualbox.org. +# +# 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, in version 3 of the +# License. +# +# 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, see . +# +# SPDX-License-Identifier: GPL-3.0-only +# + +SUB_DEPTH = ../../../.. +include $(KBUILD_PATH)/subheader.kmk + + + + +# +# HGSMIGuestR0Lib - For guest additions drivers. +# +ifdef VBOX_WITH_ADDITION_DRIVERS + LIBRARIES += HGSMIGuestR0Lib + HGSMIGuestR0Lib_TEMPLATE = VBoxGuestR0DrvLib + HGSMIGuestR0Lib_DEFS = + HGSMIGuestR0Lib_INCS = $(VBOX_GRAPHICS_INCS) + HGSMIGuestR0Lib_SOURCES = \ + HGSMICommon.cpp \ + HGSMIMemAlloc.cpp +endif + +# +# HGSMIHostLib - For host devices (R3 only). +# +LIBRARIES += HGSMIHostR3Lib +HGSMIHostR3Lib_TEMPLATE = VBoxR3Dll +HGSMIHostR3Lib_DEFS = +HGSMIHostR3Lib_INCS = $(VBOX_GRAPHICS_INCS) +HGSMIHostR3Lib_SOURCES = \ + HGSMICommon.cpp \ + HGSMIMemAlloc.cpp + +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/GuestHost/Makefile.kmk b/src/VBox/GuestHost/Makefile.kmk new file mode 100644 index 00000000..ab376bad --- /dev/null +++ b/src/VBox/GuestHost/Makefile.kmk @@ -0,0 +1,45 @@ +# $Id: Makefile.kmk $ +## @file +# Sub-Makefile for the VirtualBox Guest/Host part +# + +# +# Copyright (C) 2008-2023 Oracle and/or its affiliates. +# +# This file is part of VirtualBox base platform packages, as +# available from https://www.virtualbox.org. +# +# 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, in version 3 of the +# License. +# +# 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, see . +# +# SPDX-License-Identifier: GPL-3.0-only +# + +SUB_DEPTH = ../../.. +include $(KBUILD_PATH)/subheader.kmk + +# Include sub-makefile(s). +ifdef VBOX_WITH_HGSMI + include $(PATH_SUB_CURRENT)/HGSMI/Makefile.kmk +endif + +ifdef VBOX_WITH_DRAG_AND_DROP + include $(PATH_SUB_CURRENT)/DragAndDrop/Makefile.kmk +endif + +ifdef VBOX_WITH_HGCM + include $(PATH_SUB_CURRENT)/SharedClipboard/Makefile.kmk +endif + +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/GuestHost/SharedClipboard/ClipboardDataObjectImpl-win.cpp b/src/VBox/GuestHost/SharedClipboard/ClipboardDataObjectImpl-win.cpp new file mode 100644 index 00000000..edf9d6e7 --- /dev/null +++ b/src/VBox/GuestHost/SharedClipboard/ClipboardDataObjectImpl-win.cpp @@ -0,0 +1,978 @@ +/* $Id: ClipboardDataObjectImpl-win.cpp $ */ +/** @file + * ClipboardDataObjectImpl-win.cpp - Shared Clipboard IDataObject implementation. + */ + +/* + * Copyright (C) 2019-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_SHARED_CLIPBOARD +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +/** @todo Also handle Unicode entries. + * !!! WARNING: Buggy, doesn't work yet (some memory corruption / garbage in the file name descriptions) !!! */ +//#define VBOX_CLIPBOARD_WITH_UNICODE_SUPPORT 1 + +SharedClipboardWinDataObject::SharedClipboardWinDataObject(PSHCLTRANSFER pTransfer, + LPFORMATETC pFormatEtc, LPSTGMEDIUM pStgMed, ULONG cFormats) + : m_enmStatus(Uninitialized) + , m_lRefCount(0) + , m_cFormats(0) + , m_pTransfer(pTransfer) + , m_pStream(NULL) + , m_uObjIdx(0) + , m_fRunning(false) + , m_EventListComplete(NIL_RTSEMEVENT) + , m_EventTransferComplete(NIL_RTSEMEVENT) +{ + AssertPtr(m_pTransfer); + + HRESULT hr; + + ULONG cFixedFormats = 3; /* CFSTR_FILEDESCRIPTORA + CFSTR_FILECONTENTS + CFSTR_PERFORMEDDROPEFFECT */ +#ifdef VBOX_CLIPBOARD_WITH_UNICODE_SUPPORT + cFixedFormats++; /* CFSTR_FILEDESCRIPTORW */ +#endif + const ULONG cAllFormats = cFormats + cFixedFormats; + + try + { + m_pFormatEtc = new FORMATETC[cAllFormats]; + RT_BZERO(m_pFormatEtc, sizeof(FORMATETC) * cAllFormats); + m_pStgMedium = new STGMEDIUM[cAllFormats]; + RT_BZERO(m_pStgMedium, sizeof(STGMEDIUM) * cAllFormats); + + /** @todo Do we need CFSTR_FILENAME / CFSTR_SHELLIDLIST here? */ + + /* + * Register fixed formats. + */ + unsigned uIdx = 0; + + LogFlowFunc(("Registering CFSTR_FILEDESCRIPTORA ...\n")); + m_cfFileDescriptorA = RegisterClipboardFormat(CFSTR_FILEDESCRIPTORA); + registerFormat(&m_pFormatEtc[uIdx++], m_cfFileDescriptorA); +#ifdef VBOX_CLIPBOARD_WITH_UNICODE_SUPPORT + LogFlowFunc(("Registering CFSTR_FILEDESCRIPTORW ...\n")); + m_cfFileDescriptorW = RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW); + registerFormat(&m_pFormatEtc[uIdx++], m_cfFileDescriptorW); +#endif + + /* IStream interface, implemented in ClipboardStreamImpl-win.cpp. */ + LogFlowFunc(("Registering CFSTR_FILECONTENTS ...\n")); + m_cfFileContents = RegisterClipboardFormat(CFSTR_FILECONTENTS); + registerFormat(&m_pFormatEtc[uIdx++], m_cfFileContents, TYMED_ISTREAM, 0 /* lIndex */); + + /* We want to know from the target what the outcome of the operation was to react accordingly (e.g. abort a transfer). */ + LogFlowFunc(("Registering CFSTR_PERFORMEDDROPEFFECT ...\n")); + m_cfPerformedDropEffect = RegisterClipboardFormat(CFSTR_PERFORMEDDROPEFFECT); + registerFormat(&m_pFormatEtc[uIdx++], m_cfPerformedDropEffect, TYMED_HGLOBAL, -1 /* lIndex */, DVASPECT_CONTENT); + + /* + * Registration of dynamic formats needed? + */ + LogFlowFunc(("%RU32 dynamic formats\n", cFormats)); + if (cFormats) + { + AssertPtr(pFormatEtc); + AssertPtr(pStgMed); + + for (ULONG i = 0; i < cFormats; i++) + { + LogFlowFunc(("Format %RU32: cfFormat=%RI16, tyMed=%RU32, dwAspect=%RU32\n", + i, pFormatEtc[i].cfFormat, pFormatEtc[i].tymed, pFormatEtc[i].dwAspect)); + m_pFormatEtc[cFixedFormats + i] = pFormatEtc[i]; + m_pStgMedium[cFixedFormats + i] = pStgMed[i]; + } + } + + hr = S_OK; + } + catch (std::bad_alloc &) + { + hr = E_OUTOFMEMORY; + } + + if (SUCCEEDED(hr)) + { + m_cFormats = cAllFormats; + m_enmStatus = Initialized; + + int rc2 = RTSemEventCreate(&m_EventListComplete); + AssertRC(rc2); + rc2 = RTSemEventCreate(&m_EventTransferComplete); + AssertRC(rc2); + } + + LogFlowFunc(("cAllFormats=%RU32, hr=%Rhrc\n", cAllFormats, hr)); +} + +SharedClipboardWinDataObject::~SharedClipboardWinDataObject(void) +{ + LogFlowFuncEnter(); + + RTSemEventDestroy(m_EventListComplete); + m_EventListComplete = NIL_RTSEMEVENT; + + RTSemEventDestroy(m_EventTransferComplete); + m_EventTransferComplete = NIL_RTSEMEVENT; + + if (m_pStream) + m_pStream->Release(); + + if (m_pFormatEtc) + delete[] m_pFormatEtc; + + if (m_pStgMedium) + delete[] m_pStgMedium; + + LogFlowFunc(("mRefCount=%RI32\n", m_lRefCount)); +} + +/* + * IUnknown methods. + */ + +STDMETHODIMP_(ULONG) SharedClipboardWinDataObject::AddRef(void) +{ + LONG lCount = InterlockedIncrement(&m_lRefCount); + LogFlowFunc(("lCount=%RI32\n", lCount)); + return lCount; +} + +STDMETHODIMP_(ULONG) SharedClipboardWinDataObject::Release(void) +{ + LONG lCount = InterlockedDecrement(&m_lRefCount); + LogFlowFunc(("lCount=%RI32\n", m_lRefCount)); + if (lCount == 0) + { + delete this; + return 0; + } + + return lCount; +} + +STDMETHODIMP SharedClipboardWinDataObject::QueryInterface(REFIID iid, void **ppvObject) +{ + AssertPtrReturn(ppvObject, E_INVALIDARG); + + if ( iid == IID_IDataObject + || iid == IID_IUnknown) + { + AddRef(); + *ppvObject = this; + return S_OK; + } + + *ppvObject = 0; + return E_NOINTERFACE; +} + +/** + * Copies a chunk of data into a HGLOBAL object. + * + * @returns VBox status code. + * @param pvData Data to copy. + * @param cbData Size (in bytes) to copy. + * @param fFlags GlobalAlloc flags, used for allocating the HGLOBAL block. + * @param phGlobal Where to store the allocated HGLOBAL object. + */ +int SharedClipboardWinDataObject::copyToHGlobal(const void *pvData, size_t cbData, UINT fFlags, HGLOBAL *phGlobal) +{ + AssertPtrReturn(phGlobal, VERR_INVALID_POINTER); + + HGLOBAL hGlobal = GlobalAlloc(fFlags, cbData); + if (!hGlobal) + return VERR_NO_MEMORY; + + void *pvAlloc = GlobalLock(hGlobal); + if (pvAlloc) + { + CopyMemory(pvAlloc, pvData, cbData); + GlobalUnlock(hGlobal); + + *phGlobal = hGlobal; + + return VINF_SUCCESS; + } + + GlobalFree(hGlobal); + return VERR_ACCESS_DENIED; +} + +/** + * Reads (handles) a specific directory reursively and inserts its entry into the + * objects's entry list. + * + * @returns VBox status code. + * @param pTransfer Shared Clipboard transfer object to handle. + * @param strDir Directory path to handle. + */ +int SharedClipboardWinDataObject::readDir(PSHCLTRANSFER pTransfer, const Utf8Str &strDir) +{ + LogFlowFunc(("strDir=%s\n", strDir.c_str())); + + SHCLLISTOPENPARMS openParmsList; + int rc = ShClTransferListOpenParmsInit(&openParmsList); + if (RT_SUCCESS(rc)) + { + rc = RTStrCopy(openParmsList.pszPath, openParmsList.cbPath, strDir.c_str()); + if (RT_SUCCESS(rc)) + { + SHCLLISTHANDLE hList; + rc = ShClTransferListOpen(pTransfer, &openParmsList, &hList); + if (RT_SUCCESS(rc)) + { + LogFlowFunc(("strDir=%s -> hList=%RU64\n", strDir.c_str(), hList)); + + SHCLLISTHDR hdrList; + rc = ShClTransferListGetHeader(pTransfer, hList, &hdrList); + if (RT_SUCCESS(rc)) + { + LogFlowFunc(("cTotalObjects=%RU64, cbTotalSize=%RU64\n\n", + hdrList.cTotalObjects, hdrList.cbTotalSize)); + + for (uint64_t o = 0; o < hdrList.cTotalObjects; o++) + { + SHCLLISTENTRY entryList; + rc = ShClTransferListEntryInit(&entryList); + if (RT_SUCCESS(rc)) + { + rc = ShClTransferListRead(pTransfer, hList, &entryList); + if (RT_SUCCESS(rc)) + { + if (ShClTransferListEntryIsValid(&entryList)) + { + PSHCLFSOBJINFO pFsObjInfo = (PSHCLFSOBJINFO)entryList.pvInfo; + Assert(entryList.cbInfo == sizeof(SHCLFSOBJINFO)); + + Utf8Str strPath = strDir + Utf8Str("\\") + Utf8Str(entryList.pszName); + + LogFlowFunc(("\t%s (%RU64 bytes) -> %s\n", + entryList.pszName, pFsObjInfo->cbObject, strPath.c_str())); + + if (RTFS_IS_DIRECTORY(pFsObjInfo->Attr.fMode)) + { + FSOBJENTRY objEntry = { strPath.c_str(), *pFsObjInfo }; + + m_lstEntries.push_back(objEntry); /** @todo Can this throw? */ + + rc = readDir(pTransfer, strPath.c_str()); + } + else if (RTFS_IS_FILE(pFsObjInfo->Attr.fMode)) + { + FSOBJENTRY objEntry = { strPath.c_str(), *pFsObjInfo }; + + m_lstEntries.push_back(objEntry); /** @todo Can this throw? */ + } + else + rc = VERR_NOT_SUPPORTED; + + /** @todo Handle symlinks. */ + } + else + rc = VERR_INVALID_PARAMETER; + } + + ShClTransferListEntryDestroy(&entryList); + } + + if ( RT_FAILURE(rc) + && pTransfer->Thread.fStop) + break; + } + } + + ShClTransferListClose(pTransfer, hList); + } + } + + ShClTransferListOpenParmsDestroy(&openParmsList); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Thread for reading transfer data. + * The data object needs the (high level, root) transfer listing at the time of ::GetData(), so we need + * to block and wait until we have this data (via this thread) and continue. + * + * @returns VBox status code. + * @param ThreadSelf Thread handle. Unused at the moment. + * @param pvUser Pointer to user-provided data. Of type SharedClipboardWinDataObject. + */ +/* static */ +DECLCALLBACK(int) SharedClipboardWinDataObject::readThread(RTTHREAD ThreadSelf, void *pvUser) +{ + RT_NOREF(ThreadSelf); + + LogFlowFuncEnter(); + + SharedClipboardWinDataObject *pThis = (SharedClipboardWinDataObject *)pvUser; + + PSHCLTRANSFER pTransfer = pThis->m_pTransfer; + AssertPtr(pTransfer); + + pTransfer->Thread.fStarted = true; + pTransfer->Thread.fStop = false; + + RTThreadUserSignal(RTThreadSelf()); + + LogRel2(("Shared Clipboard: Calculating transfer ...\n")); + + PSHCLROOTLIST pRootList; + int rc = ShClTransferRootsGet(pTransfer, &pRootList); + if (RT_SUCCESS(rc)) + { + LogFlowFunc(("cRoots=%RU32\n\n", pRootList->Hdr.cRoots)); + + for (uint32_t i = 0; i < pRootList->Hdr.cRoots; i++) + { + PSHCLLISTENTRY pRootEntry = &pRootList->paEntries[i]; + AssertPtr(pRootEntry); + + Assert(pRootEntry->cbInfo == sizeof(SHCLFSOBJINFO)); + PSHCLFSOBJINFO pFsObjInfo = (PSHCLFSOBJINFO)pRootEntry->pvInfo; + + LogFlowFunc(("pszRoot=%s, fMode=0x%x\n", pRootEntry->pszName, pFsObjInfo->Attr.fMode)); + + if (RTFS_IS_DIRECTORY(pFsObjInfo->Attr.fMode)) + { + FSOBJENTRY objEntry = { pRootEntry->pszName, *pFsObjInfo }; + + pThis->m_lstEntries.push_back(objEntry); /** @todo Can this throw? */ + + rc = pThis->readDir(pTransfer, pRootEntry->pszName); + } + else if (RTFS_IS_FILE(pFsObjInfo->Attr.fMode)) + { + FSOBJENTRY objEntry = { pRootEntry->pszName, *pFsObjInfo }; + + pThis->m_lstEntries.push_back(objEntry); /** @todo Can this throw? */ + } + else + rc = VERR_NOT_SUPPORTED; + + if (ASMAtomicReadBool(&pTransfer->Thread.fStop)) + { + LogRel2(("Shared Clipboard: Stopping transfer calculation ...\n")); + break; + } + + if (RT_FAILURE(rc)) + break; + } + + ShClTransferRootListFree(pRootList); + pRootList = NULL; + + if ( RT_SUCCESS(rc) + && !ASMAtomicReadBool(&pTransfer->Thread.fStop)) + { + LogRel2(("Shared Clipboard: Transfer calculation complete (%zu root entries)\n", pThis->m_lstEntries.size())); + + /* + * Signal the "list complete" event so that this data object can return (valid) data via ::GetData(). + * This in turn then will create IStream instances (by the OS) for each file system object to handle. + */ + int rc2 = RTSemEventSignal(pThis->m_EventListComplete); + AssertRC(rc2); + + if (pThis->m_lstEntries.size()) + { + LogRel2(("Shared Clipboard: Waiting for transfer to complete ...\n")); + + LogFlowFunc(("Waiting for transfer to complete ...\n")); + + /* Transferring stuff can take a while, so don't use any timeout here. */ + rc2 = RTSemEventWait(pThis->m_EventTransferComplete, RT_INDEFINITE_WAIT); + AssertRC(rc2); + + switch (pThis->m_enmStatus) + { + case Completed: + LogRel2(("Shared Clipboard: Transfer complete\n")); + break; + + case Canceled: + LogRel2(("Shared Clipboard: Transfer canceled\n")); + break; + + case Error: + LogRel2(("Shared Clipboard: Transfer error occurred\n")); + break; + + default: + break; + } + } + else + LogRel(("Shared Clipboard: No transfer root entries found -- should not happen, please file a bug report\n")); + } + else if (RT_FAILURE(rc)) + LogRel(("Shared Clipboard: Transfer failed with %Rrc\n", rc)); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Creates a FILEGROUPDESCRIPTOR object from a given Shared Clipboard transfer and stores the result into an HGLOBAL object. + * + * @returns VBox status code. + * @param pTransfer Shared Clipboard transfer to create file grou desciprtor for. + * @param fUnicode Whether the FILEGROUPDESCRIPTOR object shall contain Unicode data or not. + * @param phGlobal Where to store the allocated HGLOBAL object on success. + */ +int SharedClipboardWinDataObject::createFileGroupDescriptorFromTransfer(PSHCLTRANSFER pTransfer, + bool fUnicode, HGLOBAL *phGlobal) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + AssertPtrReturn(phGlobal, VERR_INVALID_POINTER); + + LogFlowFuncEnter(); + + const size_t cbFileGroupDescriptor = fUnicode ? sizeof(FILEGROUPDESCRIPTORW) : sizeof(FILEGROUPDESCRIPTORA); + const size_t cbFileDescriptor = fUnicode ? sizeof(FILEDESCRIPTORW) : sizeof(FILEDESCRIPTORA); + + const UINT cItems = (UINT)m_lstEntries.size(); /** UINT vs. size_t. */ + if (!cItems) + return VERR_NOT_FOUND; + + UINT curIdx = 0; /* Current index of the handled file group descriptor (FGD). */ + + const size_t cbFGD = cbFileGroupDescriptor + (cbFileDescriptor * (cItems - 1)); + + LogFunc(("fUnicode=%RTbool, cItems=%u, cbFileDescriptor=%zu\n", fUnicode, cItems, cbFileDescriptor)); + + /* FILEGROUPDESCRIPTORA / FILEGROUPDESCRIPTOR matches except the cFileName member (TCHAR vs. WCHAR). */ + FILEGROUPDESCRIPTOR *pFGD = (FILEGROUPDESCRIPTOR *)RTMemAllocZ(cbFGD); + if (!pFGD) + return VERR_NO_MEMORY; + + int rc = VINF_SUCCESS; + + pFGD->cItems = cItems; + + char *pszFileSpec = NULL; + + FsObjEntryList::const_iterator itRoot = m_lstEntries.begin(); + while (itRoot != m_lstEntries.end()) + { + FILEDESCRIPTOR *pFD = &pFGD->fgd[curIdx]; + RT_BZERO(pFD, cbFileDescriptor); + + const char *pszFile = itRoot->strPath.c_str(); + AssertPtr(pszFile); + + pszFileSpec = RTStrDup(pszFile); + AssertBreakStmt(pszFileSpec != NULL, rc = VERR_NO_MEMORY); + + if (fUnicode) + { + PRTUTF16 pwszFileSpec; + rc = RTStrToUtf16(pszFileSpec, &pwszFileSpec); + if (RT_SUCCESS(rc)) + { + rc = RTUtf16CopyEx((PRTUTF16 )pFD->cFileName, sizeof(pFD->cFileName) / sizeof(WCHAR), + pwszFileSpec, RTUtf16Len(pwszFileSpec)); + RTUtf16Free(pwszFileSpec); + + LogFlowFunc(("pFD->cFileNameW=%ls\n", pFD->cFileName)); + } + } + else + { + rc = RTStrCopy(pFD->cFileName, sizeof(pFD->cFileName), pszFileSpec); + LogFlowFunc(("pFD->cFileNameA=%s\n", pFD->cFileName)); + } + + RTStrFree(pszFileSpec); + pszFileSpec = NULL; + + if (RT_FAILURE(rc)) + break; + + pFD->dwFlags = FD_PROGRESSUI | FD_ATTRIBUTES; + if (fUnicode) /** @todo Only >= Vista. */ + pFD->dwFlags |= FD_UNICODE; + pFD->dwFileAttributes = FILE_ATTRIBUTE_NORMAL; + + const SHCLFSOBJINFO *pObjInfo = &itRoot->objInfo; + + if (RTFS_IS_DIRECTORY(pObjInfo->Attr.fMode)) + { + pFD->dwFileAttributes |= FILE_ATTRIBUTE_DIRECTORY; + } + else if (RTFS_IS_FILE(pObjInfo->Attr.fMode)) + { + pFD->dwFlags |= FD_FILESIZE; + + const uint64_t cbObjSize = pObjInfo->cbObject; + + pFD->nFileSizeHigh = RT_HI_U32(cbObjSize); + pFD->nFileSizeLow = RT_LO_U32(cbObjSize); + } + else if (RTFS_IS_SYMLINK(pObjInfo->Attr.fMode)) + { + /** @todo Implement. */ + } +#if 0 /** @todo Implement this. */ + pFD->dwFlags = FD_ATTRIBUTES | FD_CREATETIME | FD_ACCESSTIME | FD_WRITESTIME | FD_FILESIZE; + pFD->dwFileAttributes = + pFD->ftCreationTime = + pFD->ftLastAccessTime = + pFD->ftLastWriteTime = +#endif + ++curIdx; + ++itRoot; + } + + if (pszFileSpec) + RTStrFree(pszFileSpec); + + if (RT_SUCCESS(rc)) + rc = copyToHGlobal(pFGD, cbFGD, GMEM_MOVEABLE, phGlobal); + + RTMemFree(pFGD); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Retrieves the data stored in this object and store the result in + * pMedium. + * + * @return IPRT status code. + * @return HRESULT + * @param pFormatEtc + * @param pMedium + */ +STDMETHODIMP SharedClipboardWinDataObject::GetData(LPFORMATETC pFormatEtc, LPSTGMEDIUM pMedium) +{ + AssertPtrReturn(pFormatEtc, DV_E_FORMATETC); + AssertPtrReturn(pMedium, DV_E_FORMATETC); + + LogFlowFuncEnter(); + + LogFlowFunc(("lIndex=%RI32\n", pFormatEtc->lindex)); + + /* + * Initialize default values. + */ + RT_BZERO(pMedium, sizeof(STGMEDIUM)); + + HRESULT hr = DV_E_FORMATETC; /* Play safe. */ + + if ( pFormatEtc->cfFormat == m_cfFileDescriptorA +#ifdef VBOX_CLIPBOARD_WITH_UNICODE_SUPPORT + || pFormatEtc->cfFormat == m_cfFileDescriptorW +#endif + ) + { + const bool fUnicode = pFormatEtc->cfFormat == m_cfFileDescriptorW; + + const uint32_t enmTransferStatus = ShClTransferGetStatus(m_pTransfer); + RT_NOREF(enmTransferStatus); + + LogFlowFunc(("FormatIndex_FileDescriptor%s, enmTransferStatus=%s, m_fRunning=%RTbool\n", + fUnicode ? "W" : "A", ShClTransferStatusToStr(enmTransferStatus), m_fRunning)); + + int rc; + + /* The caller can call GetData() several times, so make sure we don't do the same transfer multiple times. */ + if (!m_fRunning) + { + /* Start the transfer asynchronously in a separate thread. */ + rc = ShClTransferRun(m_pTransfer, &SharedClipboardWinDataObject::readThread, this); + if (RT_SUCCESS(rc)) + { + m_fRunning = true; + + /* Don't block for too long here, as this also will screw other apps running on the OS. */ + LogFunc(("Waiting for listing to arrive ...\n")); + rc = RTSemEventWait(m_EventListComplete, 30 * 1000 /* 30s timeout */); + if (RT_SUCCESS(rc)) + { + LogFunc(("Listing complete\n")); + } + } + } + else + rc = VINF_SUCCESS; + + if (RT_SUCCESS(rc)) + { + HGLOBAL hGlobal; + rc = createFileGroupDescriptorFromTransfer(m_pTransfer, fUnicode, &hGlobal); + if (RT_SUCCESS(rc)) + { + pMedium->tymed = TYMED_HGLOBAL; + pMedium->hGlobal = hGlobal; + /* Note: hGlobal now is being owned by pMedium / the caller. */ + + hr = S_OK; + } + else /* We can't tell any better to the caller, unfortunately. */ + hr = E_UNEXPECTED; + } + + if (RT_FAILURE(rc)) + LogRel(("Shared Clipboard: Data object unable to get data, rc=%Rrc\n", rc)); + } + + if (pFormatEtc->cfFormat == m_cfFileContents) + { + if ( pFormatEtc->lindex >= 0 + && (ULONG)pFormatEtc->lindex < m_lstEntries.size()) + { + m_uObjIdx = pFormatEtc->lindex; /* lIndex of FormatEtc contains the actual index to the object being handled. */ + + FSOBJENTRY &fsObjEntry = m_lstEntries.at(m_uObjIdx); + + LogFlowFunc(("FormatIndex_FileContents: m_uObjIdx=%u (entry '%s')\n", m_uObjIdx, fsObjEntry.strPath.c_str())); + + LogRel2(("Shared Clipboard: Receiving object '%s' ...\n", fsObjEntry.strPath.c_str())); + + /* Hand-in the provider so that our IStream implementation can continue working with it. */ + hr = SharedClipboardWinStreamImpl::Create(this /* pParent */, m_pTransfer, + fsObjEntry.strPath.c_str()/* File name */, &fsObjEntry.objInfo /* PSHCLFSOBJINFO */, + &m_pStream); + if (SUCCEEDED(hr)) + { + /* Hand over the stream to the caller. */ + pMedium->tymed = TYMED_ISTREAM; + pMedium->pstm = m_pStream; + } + } + } + else if (pFormatEtc->cfFormat == m_cfPerformedDropEffect) + { + HGLOBAL hGlobal = GlobalAlloc(GHND, sizeof(DWORD)); + + DWORD* pdwDropEffect = (DWORD*)GlobalLock(hGlobal); + *pdwDropEffect = DROPEFFECT_COPY; + + GlobalUnlock(hGlobal); + + pMedium->tymed = TYMED_HGLOBAL; + pMedium->hGlobal = hGlobal; + pMedium->pUnkForRelease = NULL; + } + + if ( FAILED(hr) + && hr != DV_E_FORMATETC) /* Can happen if the caller queries unknown / unhandled formats. */ + { + LogRel(("Shared Clipboard: Error returning data from data object (%Rhrc)\n", hr)); + } + + LogFlowFunc(("hr=%Rhrc\n", hr)); + return hr; +} + +/** + * Only required for IStream / IStorage interfaces. + * + * @return IPRT status code. + * @return HRESULT + * @param pFormatEtc + * @param pMedium + */ +STDMETHODIMP SharedClipboardWinDataObject::GetDataHere(LPFORMATETC pFormatEtc, LPSTGMEDIUM pMedium) +{ + RT_NOREF(pFormatEtc, pMedium); + LogFlowFunc(("\n")); + return E_NOTIMPL; +} + +/** + * Query if this objects supports a specific format. + * + * @return IPRT status code. + * @return HRESULT + * @param pFormatEtc + */ +STDMETHODIMP SharedClipboardWinDataObject::QueryGetData(LPFORMATETC pFormatEtc) +{ + LogFlowFunc(("\n")); + return lookupFormatEtc(pFormatEtc, NULL /* puIndex */) ? S_OK : DV_E_FORMATETC; +} + +STDMETHODIMP SharedClipboardWinDataObject::GetCanonicalFormatEtc(LPFORMATETC pFormatEtc, LPFORMATETC pFormatEtcOut) +{ + RT_NOREF(pFormatEtc); + LogFlowFunc(("\n")); + + /* Set this to NULL in any case. */ + pFormatEtcOut->ptd = NULL; + return E_NOTIMPL; +} + +STDMETHODIMP SharedClipboardWinDataObject::SetData(LPFORMATETC pFormatEtc, LPSTGMEDIUM pMedium, BOOL fRelease) +{ + if ( pFormatEtc == NULL + || pMedium == NULL) + return E_INVALIDARG; + + if (pFormatEtc->lindex != -1) + return DV_E_LINDEX; + + if (pFormatEtc->tymed != TYMED_HGLOBAL) + return DV_E_TYMED; + + if (pFormatEtc->dwAspect != DVASPECT_CONTENT) + return DV_E_DVASPECT; + + LogFlowFunc(("cfFormat=%RU16, lookupFormatEtc=%RTbool\n", + pFormatEtc->cfFormat, lookupFormatEtc(pFormatEtc, NULL /* puIndex */))); + + /* CFSTR_PERFORMEDDROPEFFECT is used by the drop target (caller of this IDataObject) to communicate + * the outcome of the overall operation. */ + if ( pFormatEtc->cfFormat == m_cfPerformedDropEffect + && pMedium->tymed == TYMED_HGLOBAL) + { + DWORD dwEffect = *(DWORD *)GlobalLock(pMedium->hGlobal); + GlobalUnlock(pMedium->hGlobal); + + LogFlowFunc(("dwEffect=%RI32\n", dwEffect)); + + /* Did the user cancel the operation via UI (shell)? This also might happen when overwriting an existing file + * and the user doesn't want to allow this. */ + if (dwEffect == DROPEFFECT_NONE) + { + LogRel2(("Shared Clipboard: Transfer canceled by user interaction\n")); + + OnTransferCanceled(); + } + /** @todo Detect move / overwrite actions here. */ + + if (fRelease) + ReleaseStgMedium(pMedium); + + return S_OK; + } + + return E_NOTIMPL; +} + +STDMETHODIMP SharedClipboardWinDataObject::EnumFormatEtc(DWORD dwDirection, IEnumFORMATETC **ppEnumFormatEtc) +{ + LogFlowFunc(("dwDirection=%RI32, mcFormats=%RI32, mpFormatEtc=%p\n", dwDirection, m_cFormats, m_pFormatEtc)); + + HRESULT hr; + if (dwDirection == DATADIR_GET) + hr = SharedClipboardWinEnumFormatEtc::CreateEnumFormatEtc(m_cFormats, m_pFormatEtc, ppEnumFormatEtc); + else + hr = E_NOTIMPL; + + LogFlowFunc(("hr=%Rhrc\n", hr)); + return hr; +} + +STDMETHODIMP SharedClipboardWinDataObject::DAdvise(LPFORMATETC pFormatEtc, DWORD fAdvise, IAdviseSink *pAdvSink, DWORD *pdwConnection) +{ + RT_NOREF(pFormatEtc, fAdvise, pAdvSink, pdwConnection); + return OLE_E_ADVISENOTSUPPORTED; +} + +STDMETHODIMP SharedClipboardWinDataObject::DUnadvise(DWORD dwConnection) +{ + RT_NOREF(dwConnection); + return OLE_E_ADVISENOTSUPPORTED; +} + +STDMETHODIMP SharedClipboardWinDataObject::EnumDAdvise(IEnumSTATDATA **ppEnumAdvise) +{ + RT_NOREF(ppEnumAdvise); + return OLE_E_ADVISENOTSUPPORTED; +} + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_WIN_ASYNC +/* + * IDataObjectAsyncCapability methods. + */ + +STDMETHODIMP SharedClipboardWinDataObject::EndOperation(HRESULT hResult, IBindCtx *pbcReserved, DWORD dwEffects) +{ + RT_NOREF(hResult, pbcReserved, dwEffects); + return E_NOTIMPL; +} + +STDMETHODIMP SharedClipboardWinDataObject::GetAsyncMode(BOOL *pfIsOpAsync) +{ + RT_NOREF(pfIsOpAsync); + return E_NOTIMPL; +} + +STDMETHODIMP SharedClipboardWinDataObject::InOperation(BOOL *pfInAsyncOp) +{ + RT_NOREF(pfInAsyncOp); + return E_NOTIMPL; +} + +STDMETHODIMP SharedClipboardWinDataObject::SetAsyncMode(BOOL fDoOpAsync) +{ + RT_NOREF(fDoOpAsync); + return E_NOTIMPL; +} + +STDMETHODIMP SharedClipboardWinDataObject::StartOperation(IBindCtx *pbcReserved) +{ + RT_NOREF(pbcReserved); + return E_NOTIMPL; +} +#endif /* VBOX_WITH_SHARED_CLIPBOARD_WIN_ASYNC */ + +/* + * Own stuff. + */ + +int SharedClipboardWinDataObject::Init(void) +{ + LogFlowFuncLeaveRC(VINF_SUCCESS); + return VINF_SUCCESS; +} + +void SharedClipboardWinDataObject::OnTransferComplete(int rc /* = VINF_SUCESS */) +{ + RT_NOREF(rc); + + LogFlowFunc(("m_uObjIdx=%RU32 (total: %zu)\n", m_uObjIdx, m_lstEntries.size())); + + if (RT_SUCCESS(rc)) + { + const bool fComplete = m_uObjIdx == m_lstEntries.size() - 1 /* Object index is zero-based */; + if (fComplete) + { + m_enmStatus = Completed; + } + } + else + m_enmStatus = Error; + + if (m_enmStatus != Initialized) + { + if (m_EventTransferComplete != NIL_RTSEMEVENT) + { + int rc2 = RTSemEventSignal(m_EventTransferComplete); + AssertRC(rc2); + } + } + + LogFlowFuncLeaveRC(rc); +} + +void SharedClipboardWinDataObject::OnTransferCanceled(void) +{ + LogFlowFuncEnter(); + + m_enmStatus = Canceled; + + if (m_EventTransferComplete != NIL_RTSEMEVENT) + { + int rc2 = RTSemEventSignal(m_EventTransferComplete); + AssertRC(rc2); + } + + LogFlowFuncLeave(); +} + +/* static */ +void SharedClipboardWinDataObject::logFormat(CLIPFORMAT fmt) +{ + char szFormat[128]; + if (GetClipboardFormatName(fmt, szFormat, sizeof(szFormat))) + { + LogFlowFunc(("clipFormat=%RI16 -> %s\n", fmt, szFormat)); + } + else + LogFlowFunc(("clipFormat=%RI16 is unknown\n", fmt)); +} + +bool SharedClipboardWinDataObject::lookupFormatEtc(LPFORMATETC pFormatEtc, ULONG *puIndex) +{ + AssertReturn(pFormatEtc, false); + /* puIndex is optional. */ + + for (ULONG i = 0; i < m_cFormats; i++) + { + if( (pFormatEtc->tymed & m_pFormatEtc[i].tymed) + && pFormatEtc->cfFormat == m_pFormatEtc[i].cfFormat) + /* Note: Do *not* compare dwAspect here, as this can be dynamic, depending on how the object should be represented. */ + //&& pFormatEtc->dwAspect == m_pFormatEtc[i].dwAspect) + { + LogRel2(("Shared Clipboard: Format found: tyMed=%RI32, cfFormat=%RI16, dwAspect=%RI32, ulIndex=%RU32\n", + pFormatEtc->tymed, pFormatEtc->cfFormat, pFormatEtc->dwAspect, i)); + if (puIndex) + *puIndex = i; + return true; + } + } + + LogRel2(("Shared Clipboard: Format NOT found: tyMed=%RI32, cfFormat=%RI16, dwAspect=%RI32\n", + pFormatEtc->tymed, pFormatEtc->cfFormat, pFormatEtc->dwAspect)); + + logFormat(pFormatEtc->cfFormat); + + return false; +} + +void SharedClipboardWinDataObject::registerFormat(LPFORMATETC pFormatEtc, CLIPFORMAT clipFormat, + TYMED tyMed, LONG lIndex, DWORD dwAspect, + DVTARGETDEVICE *pTargetDevice) +{ + AssertPtr(pFormatEtc); + + pFormatEtc->cfFormat = clipFormat; + pFormatEtc->tymed = tyMed; + pFormatEtc->lindex = lIndex; + pFormatEtc->dwAspect = dwAspect; + pFormatEtc->ptd = pTargetDevice; + + LogFlowFunc(("Registered format=%ld\n", pFormatEtc->cfFormat)); + + logFormat(pFormatEtc->cfFormat); +} diff --git a/src/VBox/GuestHost/SharedClipboard/ClipboardEnumFormatEtcImpl-win.cpp b/src/VBox/GuestHost/SharedClipboard/ClipboardEnumFormatEtcImpl-win.cpp new file mode 100644 index 00000000..3e517a90 --- /dev/null +++ b/src/VBox/GuestHost/SharedClipboard/ClipboardEnumFormatEtcImpl-win.cpp @@ -0,0 +1,205 @@ +/* $Id: ClipboardEnumFormatEtcImpl-win.cpp $ */ +/** @file + * ClipboardEnumFormatEtcImpl-win.cpp - Shared Clipboard IEnumFORMATETC ("Format et cetera") implementation. + */ + +/* + * Copyright (C) 2013-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include /* For bad_alloc. */ + +#define LOG_GROUP LOG_GROUP_SHARED_CLIPBOARD +#include + +#include + +#include + + + +SharedClipboardWinEnumFormatEtc::SharedClipboardWinEnumFormatEtc(LPFORMATETC pFormatEtc, ULONG cFormats) + : m_lRefCount(1), + m_nIndex(0) +{ + HRESULT hr; + + try + { + LogFlowFunc(("pFormatEtc=%p, cFormats=%RU32\n", pFormatEtc, cFormats)); + m_pFormatEtc = new FORMATETC[cFormats]; + + for (ULONG i = 0; i < cFormats; i++) + { + LogFlowFunc(("Format %RU32: cfFormat=%RI16, tyMed=%RU32, dwAspect=%RU32\n", + i, pFormatEtc[i].cfFormat, pFormatEtc[i].tymed, pFormatEtc[i].dwAspect)); + + SharedClipboardWinDataObject::logFormat(pFormatEtc[i].cfFormat); + + SharedClipboardWinEnumFormatEtc::CopyFormat(&m_pFormatEtc[i], &pFormatEtc[i]); + } + + m_nNumFormats = cFormats; + hr = S_OK; + } + catch (std::bad_alloc &) + { + hr = E_OUTOFMEMORY; + } + + LogFlowFunc(("hr=%Rhrc\n", hr)); +} + +SharedClipboardWinEnumFormatEtc::~SharedClipboardWinEnumFormatEtc(void) +{ + if (m_pFormatEtc) + { + for (ULONG i = 0; i < m_nNumFormats; i++) + { + if(m_pFormatEtc[i].ptd) + CoTaskMemFree(m_pFormatEtc[i].ptd); + } + + delete[] m_pFormatEtc; + m_pFormatEtc = NULL; + } + + LogFlowFunc(("m_lRefCount=%RI32\n", m_lRefCount)); +} + +/* + * IUnknown methods. + */ + +STDMETHODIMP_(ULONG) SharedClipboardWinEnumFormatEtc::AddRef(void) +{ + return InterlockedIncrement(&m_lRefCount); +} + +STDMETHODIMP_(ULONG) SharedClipboardWinEnumFormatEtc::Release(void) +{ + LONG lCount = InterlockedDecrement(&m_lRefCount); + if (lCount == 0) + { + LogFlowFunc(("Delete\n")); + delete this; + return 0; + } + + return lCount; +} + +STDMETHODIMP SharedClipboardWinEnumFormatEtc::QueryInterface(REFIID iid, void **ppvObject) +{ + if ( iid == IID_IEnumFORMATETC + || iid == IID_IUnknown) + { + AddRef(); + *ppvObject = this; + return S_OK; + } + + *ppvObject = 0; + return E_NOINTERFACE; +} + +STDMETHODIMP SharedClipboardWinEnumFormatEtc::Next(ULONG cFormats, LPFORMATETC pFormatEtc, ULONG *pcFetched) +{ + ULONG ulCopied = 0; + + if(cFormats == 0 || pFormatEtc == 0) + return E_INVALIDARG; + + while ( m_nIndex < m_nNumFormats + && ulCopied < cFormats) + { + SharedClipboardWinEnumFormatEtc::CopyFormat(&pFormatEtc[ulCopied], &m_pFormatEtc[m_nIndex]); + ulCopied++; + m_nIndex++; + } + + if (pcFetched) + *pcFetched = ulCopied; + + return (ulCopied == cFormats) ? S_OK : S_FALSE; +} + +STDMETHODIMP SharedClipboardWinEnumFormatEtc::Skip(ULONG cFormats) +{ + m_nIndex += cFormats; + return (m_nIndex <= m_nNumFormats) ? S_OK : S_FALSE; +} + +STDMETHODIMP SharedClipboardWinEnumFormatEtc::Reset(void) +{ + m_nIndex = 0; + return S_OK; +} + +STDMETHODIMP SharedClipboardWinEnumFormatEtc::Clone(IEnumFORMATETC **ppEnumFormatEtc) +{ + HRESULT hResult = CreateEnumFormatEtc(m_nNumFormats, m_pFormatEtc, ppEnumFormatEtc); + if (hResult == S_OK) + ((SharedClipboardWinEnumFormatEtc *) *ppEnumFormatEtc)->m_nIndex = m_nIndex; + + return hResult; +} + +/* static */ +void SharedClipboardWinEnumFormatEtc::CopyFormat(LPFORMATETC pDest, LPFORMATETC pSource) +{ + AssertPtrReturnVoid(pDest); + AssertPtrReturnVoid(pSource); + + *pDest = *pSource; + + if (pSource->ptd) + { + pDest->ptd = (DVTARGETDEVICE*)CoTaskMemAlloc(sizeof(DVTARGETDEVICE)); + *(pDest->ptd) = *(pSource->ptd); + } +} + +/* static */ +HRESULT SharedClipboardWinEnumFormatEtc::CreateEnumFormatEtc(UINT nNumFormats, LPFORMATETC pFormatEtc, IEnumFORMATETC **ppEnumFormatEtc) +{ + AssertReturn(nNumFormats, E_INVALIDARG); + AssertPtrReturn(pFormatEtc, E_INVALIDARG); + AssertPtrReturn(ppEnumFormatEtc, E_INVALIDARG); + + HRESULT hr; + try + { + *ppEnumFormatEtc = new SharedClipboardWinEnumFormatEtc(pFormatEtc, nNumFormats); + hr = S_OK; + } + catch(std::bad_alloc &) + { + hr = E_OUTOFMEMORY; + } + + return hr; +} + diff --git a/src/VBox/GuestHost/SharedClipboard/ClipboardMIME.cpp b/src/VBox/GuestHost/SharedClipboard/ClipboardMIME.cpp new file mode 100644 index 00000000..e8c800b8 --- /dev/null +++ b/src/VBox/GuestHost/SharedClipboard/ClipboardMIME.cpp @@ -0,0 +1,53 @@ +/* $Id: ClipboardMIME.cpp $ */ +/** @file + * Shared Clipboard - Path list class. + */ + +/* + * Copyright (C) 2019-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_SHARED_CLIPBOARD +#include + +#include + + +bool ShClMIMEHasFileURLs(const char *pcszFormat, size_t cchFormatMax) +{ + /** @todo "text/uri" also an official variant? */ + return ( RTStrNICmp(pcszFormat, "text/uri-list", cchFormatMax) == 0 + || RTStrNICmp(pcszFormat, "x-special/gnome-icon-list", cchFormatMax) == 0); +} + +bool ShClMIMENeedsCache(const char *pcszFormat, size_t cchFormatMax) +{ + bool fNeedsDropDir = false; + if (!RTStrNICmp(pcszFormat, "text/uri-list", cchFormatMax)) /** @todo Add "x-special/gnome-icon-list"? */ + fNeedsDropDir = true; + + return fNeedsDropDir; +} + diff --git a/src/VBox/GuestHost/SharedClipboard/ClipboardPath.cpp b/src/VBox/GuestHost/SharedClipboard/ClipboardPath.cpp new file mode 100644 index 00000000..bb9cc3d3 --- /dev/null +++ b/src/VBox/GuestHost/SharedClipboard/ClipboardPath.cpp @@ -0,0 +1,92 @@ +/* $Id: ClipboardPath.cpp $ */ +/** @file + * Shared Clipboard - Path handling. + */ + +/* + * Copyright (C) 2019-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_SHARED_CLIPBOARD +#include + +#include +#include +#include + + +/** + * Sanitizes the file name component so that unsupported characters + * will be replaced by an underscore ("_"). + * + * @return IPRT status code. + * @param pszPath Path to sanitize. + * @param cbPath Size (in bytes) of path to sanitize. + */ +int ShClPathSanitizeFilename(char *pszPath, size_t cbPath) +{ + int rc = VINF_SUCCESS; +#ifdef RT_OS_WINDOWS + RT_NOREF1(cbPath); + /* Replace out characters not allowed on Windows platforms, put in by RTTimeSpecToString(). */ + /** @todo Use something like RTPathSanitize() if available later some time. */ + static const RTUNICP s_uszValidRangePairs[] = + { + ' ', ' ', + '(', ')', + '-', '.', + '0', '9', + 'A', 'Z', + 'a', 'z', + '_', '_', + 0xa0, 0xd7af, + '\0' + }; + ssize_t cReplaced = RTStrPurgeComplementSet(pszPath, s_uszValidRangePairs, '_' /* chReplacement */); + if (cReplaced < 0) + rc = VERR_INVALID_UTF8_ENCODING; +#else + RT_NOREF2(pszPath, cbPath); +#endif + return rc; +} + +/** + * Sanitizes a given path regarding invalid / unhandled characters. + * Currently not implemented. + * + * @returns VBox status code. + * @param pszPath Path to sanitize. UTF-8. + * @param cbPath Size (in bytes) of the path to sanitize. + */ +int ShClPathSanitize(char *pszPath, size_t cbPath) +{ + RT_NOREF(pszPath, cbPath); + + /** @todo */ + + return VINF_SUCCESS; +} + diff --git a/src/VBox/GuestHost/SharedClipboard/ClipboardStreamImpl-win.cpp b/src/VBox/GuestHost/SharedClipboard/ClipboardStreamImpl-win.cpp new file mode 100644 index 00000000..38626873 --- /dev/null +++ b/src/VBox/GuestHost/SharedClipboard/ClipboardStreamImpl-win.cpp @@ -0,0 +1,382 @@ +/* $Id: ClipboardStreamImpl-win.cpp $ */ +/** @file + * ClipboardStreamImpl-win.cpp - Shared Clipboard IStream object implementation (guest and host side). + */ + +/* + * Copyright (C) 2019-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_SHARED_CLIPBOARD +#include + +#include +#include +#include + +#include +#include +#include + +#include + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ + + + +/********************************************************************************************************************************* +* Static variables * +*********************************************************************************************************************************/ + + + +SharedClipboardWinStreamImpl::SharedClipboardWinStreamImpl(SharedClipboardWinDataObject *pParent, PSHCLTRANSFER pTransfer, + const Utf8Str &strPath, PSHCLFSOBJINFO pObjInfo) + : m_pParent(pParent) + , m_lRefCount(1) /* Our IDataObjct *always* holds the last reference to this object; needed for the callbacks. */ + , m_pTransfer(pTransfer) + , m_hObj(SHCLOBJHANDLE_INVALID) + , m_strPath(strPath) + , m_objInfo(*pObjInfo) + , m_cbProcessed(0) + , m_fIsComplete(false) +{ + AssertPtr(m_pTransfer); + + LogFunc(("m_strPath=%s\n", m_strPath.c_str())); +} + +SharedClipboardWinStreamImpl::~SharedClipboardWinStreamImpl(void) +{ + LogFlowThisFuncEnter(); +} + +/* + * IUnknown methods. + */ + +STDMETHODIMP SharedClipboardWinStreamImpl::QueryInterface(REFIID iid, void **ppvObject) +{ + AssertPtrReturn(ppvObject, E_INVALIDARG); + + if (iid == IID_IUnknown) + { + LogFlowFunc(("IID_IUnknown\n")); + *ppvObject = (IUnknown *)(ISequentialStream *)this; + } + else if (iid == IID_ISequentialStream) + { + LogFlowFunc(("IID_ISequentialStream\n")); + *ppvObject = (ISequentialStream *)this; + } + else if (iid == IID_IStream) + { + LogFlowFunc(("IID_IStream\n")); + *ppvObject = (IStream *)this; + } + else + { + *ppvObject = NULL; + return E_NOINTERFACE; + } + + AddRef(); + return S_OK; +} + +STDMETHODIMP_(ULONG) SharedClipboardWinStreamImpl::AddRef(void) +{ + LONG lCount = InterlockedIncrement(&m_lRefCount); + LogFlowFunc(("lCount=%RI32\n", lCount)); + return lCount; +} + +STDMETHODIMP_(ULONG) SharedClipboardWinStreamImpl::Release(void) +{ + LONG lCount = InterlockedDecrement(&m_lRefCount); + LogFlowFunc(("lCount=%RI32\n", m_lRefCount)); + if (lCount == 0) + { + delete this; + return 0; + } + + return lCount; +} + +/* + * IStream methods. + */ + +STDMETHODIMP SharedClipboardWinStreamImpl::Clone(IStream** ppStream) +{ + RT_NOREF(ppStream); + + LogFlowFuncEnter(); + return E_NOTIMPL; +} + +STDMETHODIMP SharedClipboardWinStreamImpl::Commit(DWORD dwFrags) +{ + RT_NOREF(dwFrags); + + LogFlowThisFuncEnter(); + return E_NOTIMPL; +} + +STDMETHODIMP SharedClipboardWinStreamImpl::CopyTo(IStream *pDestStream, ULARGE_INTEGER nBytesToCopy, ULARGE_INTEGER *nBytesRead, + ULARGE_INTEGER *nBytesWritten) +{ + RT_NOREF(pDestStream, nBytesToCopy, nBytesRead, nBytesWritten); + + LogFlowThisFuncEnter(); + return E_NOTIMPL; +} + +STDMETHODIMP SharedClipboardWinStreamImpl::LockRegion(ULARGE_INTEGER nStart, ULARGE_INTEGER nBytes,DWORD dwFlags) +{ + RT_NOREF(nStart, nBytes, dwFlags); + + LogFlowThisFuncEnter(); + return STG_E_INVALIDFUNCTION; +} + +/* Note: Windows seems to assume EOF if nBytesRead < nBytesToRead. */ +STDMETHODIMP SharedClipboardWinStreamImpl::Read(void *pvBuffer, ULONG nBytesToRead, ULONG *nBytesRead) +{ + LogFlowThisFunc(("Enter: m_cbProcessed=%RU64\n", m_cbProcessed)); + + /** @todo Is there any locking required so that parallel reads aren't possible? */ + + if (!pvBuffer) + return STG_E_INVALIDPOINTER; + + if ( nBytesToRead == 0 + || m_fIsComplete) + { + if (nBytesRead) + *nBytesRead = 0; + return S_OK; + } + + int rc; + + try + { + if ( m_hObj == SHCLOBJHANDLE_INVALID + && m_pTransfer->ProviderIface.pfnObjOpen) + { + SHCLOBJOPENCREATEPARMS openParms; + rc = ShClTransferObjOpenParmsInit(&openParms); + if (RT_SUCCESS(rc)) + { + openParms.fCreate = SHCL_OBJ_CF_ACCESS_READ + | SHCL_OBJ_CF_ACCESS_DENYWRITE; + + rc = RTStrCopy(openParms.pszPath, openParms.cbPath, m_strPath.c_str()); + if (RT_SUCCESS(rc)) + { + rc = m_pTransfer->ProviderIface.pfnObjOpen(&m_pTransfer->ProviderCtx, &openParms, &m_hObj); + } + + ShClTransferObjOpenParmsDestroy(&openParms); + } + } + else + rc = VINF_SUCCESS; + + uint32_t cbRead = 0; + + const uint64_t cbSize = (uint64_t)m_objInfo.cbObject; + const uint32_t cbToRead = RT_MIN(cbSize - m_cbProcessed, nBytesToRead); + + if (RT_SUCCESS(rc)) + { + if (cbToRead) + { + rc = m_pTransfer->ProviderIface.pfnObjRead(&m_pTransfer->ProviderCtx, m_hObj, + pvBuffer, cbToRead, 0 /* fFlags */, &cbRead); + if (RT_SUCCESS(rc)) + { + m_cbProcessed += cbRead; + Assert(m_cbProcessed <= cbSize); + } + } + + /* Transfer complete? Make sure to close the object again. */ + m_fIsComplete = m_cbProcessed == cbSize; + + if (m_fIsComplete) + { + if (m_pTransfer->ProviderIface.pfnObjClose) + { + int rc2 = m_pTransfer->ProviderIface.pfnObjClose(&m_pTransfer->ProviderCtx, m_hObj); + AssertRC(rc2); + } + + if (m_pParent) + m_pParent->OnTransferComplete(); + } + } + + LogFlowThisFunc(("Leave: rc=%Rrc, cbSize=%RU64, cbProcessed=%RU64 -> nBytesToRead=%RU32, cbToRead=%RU32, cbRead=%RU32\n", + rc, cbSize, m_cbProcessed, nBytesToRead, cbToRead, cbRead)); + + if (nBytesRead) + *nBytesRead = (ULONG)cbRead; + + if (nBytesToRead != cbRead) + return S_FALSE; + + return S_OK; + } + catch (...) + { + LogFunc(("Caught exception\n")); + } + + return E_FAIL; +} + +STDMETHODIMP SharedClipboardWinStreamImpl::Revert(void) +{ + LogFlowThisFuncEnter(); + return E_NOTIMPL; +} + +STDMETHODIMP SharedClipboardWinStreamImpl::Seek(LARGE_INTEGER nMove, DWORD dwOrigin, ULARGE_INTEGER* nNewPos) +{ + RT_NOREF(nMove, dwOrigin, nNewPos); + + LogFlowThisFunc(("nMove=%RI64, dwOrigin=%RI32\n", nMove, dwOrigin)); + + return E_NOTIMPL; +} + +STDMETHODIMP SharedClipboardWinStreamImpl::SetSize(ULARGE_INTEGER nNewSize) +{ + RT_NOREF(nNewSize); + + LogFlowThisFuncEnter(); + return E_NOTIMPL; +} + +STDMETHODIMP SharedClipboardWinStreamImpl::Stat(STATSTG *pStatStg, DWORD dwFlags) +{ + HRESULT hr = S_OK; + + if (pStatStg) + { + RT_ZERO(*pStatStg); + + switch (dwFlags) + { + case STATFLAG_NONAME: + pStatStg->pwcsName = NULL; + break; + + case STATFLAG_DEFAULT: + { + /** @todo r=bird: This is using the wrong allocator. According to MSDN the + * caller will pass this to CoTaskMemFree, so we should use CoTaskMemAlloc to + * allocate it. */ + int rc2 = RTStrToUtf16(m_strPath.c_str(), &pStatStg->pwcsName); + if (RT_FAILURE(rc2)) + hr = E_FAIL; + break; + } + + default: + hr = STG_E_INVALIDFLAG; + break; + } + + if (SUCCEEDED(hr)) + { + pStatStg->type = STGTY_STREAM; + pStatStg->grfMode = STGM_READ; + pStatStg->grfLocksSupported = 0; + pStatStg->cbSize.QuadPart = (uint64_t)m_objInfo.cbObject; + } + } + else + hr = STG_E_INVALIDPOINTER; + + LogFlowThisFunc(("hr=%Rhrc\n", hr)); + return hr; +} + +STDMETHODIMP SharedClipboardWinStreamImpl::UnlockRegion(ULARGE_INTEGER nStart, ULARGE_INTEGER nBytes, DWORD dwFlags) +{ + RT_NOREF(nStart, nBytes, dwFlags); + + LogFlowThisFuncEnter(); + return E_NOTIMPL; +} + +STDMETHODIMP SharedClipboardWinStreamImpl::Write(const void *pvBuffer, ULONG nBytesToRead, ULONG *nBytesRead) +{ + RT_NOREF(pvBuffer, nBytesToRead, nBytesRead); + + LogFlowThisFuncEnter(); + return E_NOTIMPL; +} + +/* + * Own stuff. + */ + +/** + * Factory to create our own IStream implementation. + * + * @returns HRESULT + * @param pParent Pointer to the parent data object. + * @param pTransfer Pointer to Shared Clipboard transfer object to use. + * @param strPath Path of object to handle for the stream. + * @param pObjInfo Pointer to object information. + * @param ppStream Where to return the created stream object on success. + */ +/* static */ +HRESULT SharedClipboardWinStreamImpl::Create(SharedClipboardWinDataObject *pParent, PSHCLTRANSFER pTransfer, + const Utf8Str &strPath, PSHCLFSOBJINFO pObjInfo, + IStream **ppStream) +{ + AssertPtrReturn(pTransfer, E_POINTER); + + SharedClipboardWinStreamImpl *pStream = new SharedClipboardWinStreamImpl(pParent, pTransfer, strPath, pObjInfo); + if (pStream) + { + pStream->AddRef(); + + *ppStream = pStream; + return S_OK; + } + + return E_FAIL; +} + diff --git a/src/VBox/GuestHost/SharedClipboard/Makefile.kmk b/src/VBox/GuestHost/SharedClipboard/Makefile.kmk new file mode 100644 index 00000000..aa3cdc96 --- /dev/null +++ b/src/VBox/GuestHost/SharedClipboard/Makefile.kmk @@ -0,0 +1,36 @@ +# $Id: Makefile.kmk $ +## @file +# Sub-Makefile for the VirtualBox Shared Clipboard guest/host code. +# + +# +# Copyright (C) 2020-2023 Oracle and/or its affiliates. +# +# This file is part of VirtualBox base platform packages, as +# available from https://www.virtualbox.org. +# +# 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, in version 3 of the +# License. +# +# 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, see . +# +# SPDX-License-Identifier: GPL-3.0-only +# + +SUB_DEPTH = ../../../.. +include $(KBUILD_PATH)/subheader.kmk + +ifdef VBOX_WITH_TESTCASES + include $(PATH_SUB_CURRENT)/testcase/Makefile.kmk +endif + +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/GuestHost/SharedClipboard/clipboard-common.cpp b/src/VBox/GuestHost/SharedClipboard/clipboard-common.cpp new file mode 100644 index 00000000..c1886d31 --- /dev/null +++ b/src/VBox/GuestHost/SharedClipboard/clipboard-common.cpp @@ -0,0 +1,1226 @@ +/* $Id: clipboard-common.cpp $ */ +/** @file + * Shared Clipboard: Some helper function for converting between the various eol. + */ + +/* + * Includes contributions from François Revol + * + * Copyright (C) 2006-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#define LOG_GROUP LOG_GROUP_SHARED_CLIPBOARD + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + + +/********************************************************************************************************************************* +* Prototypes * +*********************************************************************************************************************************/ +static void shClEventSourceResetInternal(PSHCLEVENTSOURCE pSource); + +static void shClEventDestroy(PSHCLEVENT pEvent); +DECLINLINE(PSHCLEVENT) shclEventGet(PSHCLEVENTSOURCE pSource, SHCLEVENTID idEvent); + + +/********************************************************************************************************************************* +* Implementation * +*********************************************************************************************************************************/ + +/** + * Allocates a new event payload. + * + * @returns VBox status code. + * @param uID Payload ID to set for this payload. Useful for consequtive payloads. + * @param pvData Data block to associate to this payload. + * @param cbData Size (in bytes) of data block to associate. + * @param ppPayload Where to store the allocated event payload on success. + */ +int ShClPayloadAlloc(uint32_t uID, const void *pvData, uint32_t cbData, + PSHCLEVENTPAYLOAD *ppPayload) +{ + AssertPtrReturn(pvData, VERR_INVALID_POINTER); + AssertReturn(cbData > 0, VERR_INVALID_PARAMETER); + + PSHCLEVENTPAYLOAD pPayload = (PSHCLEVENTPAYLOAD)RTMemAlloc(sizeof(SHCLEVENTPAYLOAD)); + if (pPayload) + { + pPayload->pvData = RTMemDup(pvData, cbData); + if (pPayload->pvData) + { + pPayload->cbData = cbData; + pPayload->uID = uID; + + *ppPayload = pPayload; + return VINF_SUCCESS; + } + + RTMemFree(pPayload); + } + return VERR_NO_MEMORY; +} + +/** + * Frees an event payload. + * + * @returns VBox status code. + * @param pPayload Event payload to free. + */ +void ShClPayloadFree(PSHCLEVENTPAYLOAD pPayload) +{ + if (!pPayload) + return; + + if (pPayload->pvData) + { + Assert(pPayload->cbData); + RTMemFree(pPayload->pvData); + pPayload->pvData = NULL; + } + + pPayload->cbData = 0; + pPayload->uID = UINT32_MAX; + + RTMemFree(pPayload); +} + +/** + * Creates a new event source. + * + * @returns VBox status code. + * @param pSource Event source to create. + * @param uID ID to use for event source. + */ +int ShClEventSourceCreate(PSHCLEVENTSOURCE pSource, SHCLEVENTSOURCEID uID) +{ + LogFlowFunc(("pSource=%p, uID=%RU16\n", pSource, uID)); + AssertPtrReturn(pSource, VERR_INVALID_POINTER); + + int rc = RTCritSectInit(&pSource->CritSect); + AssertRCReturn(rc, rc); + + RTListInit(&pSource->lstEvents); + + pSource->uID = uID; + /* Choose a random event ID starting point. */ + pSource->idNextEvent = RTRandU32Ex(1, VBOX_SHCL_MAX_EVENTS - 1); + + return VINF_SUCCESS; +} + +/** + * Destroys an event source. + * + * @returns VBox status code. + * @param pSource Event source to destroy. + */ +int ShClEventSourceDestroy(PSHCLEVENTSOURCE pSource) +{ + if (!pSource) + return VINF_SUCCESS; + + LogFlowFunc(("ID=%RU32\n", pSource->uID)); + + int rc = RTCritSectEnter(&pSource->CritSect); + if (RT_SUCCESS(rc)) + { + shClEventSourceResetInternal(pSource); + + rc = RTCritSectLeave(&pSource->CritSect); + AssertRC(rc); + + RTCritSectDelete(&pSource->CritSect); + + pSource->uID = UINT16_MAX; + pSource->idNextEvent = UINT32_MAX; + } + + return rc; +} + +/** + * Resets an event source, internal version. + * + * @param pSource Event source to reset. + */ +static void shClEventSourceResetInternal(PSHCLEVENTSOURCE pSource) +{ + LogFlowFunc(("ID=%RU32\n", pSource->uID)); + + PSHCLEVENT pEvIt; + PSHCLEVENT pEvItNext; + RTListForEachSafe(&pSource->lstEvents, pEvIt, pEvItNext, SHCLEVENT, Node) + { + RTListNodeRemove(&pEvIt->Node); + + shClEventDestroy(pEvIt); + + RTMemFree(pEvIt); + pEvIt = NULL; + } +} + +/** + * Resets an event source. + * + * @param pSource Event source to reset. + */ +void ShClEventSourceReset(PSHCLEVENTSOURCE pSource) +{ + int rc2 = RTCritSectEnter(&pSource->CritSect); + if (RT_SUCCESS(rc2)) + { + shClEventSourceResetInternal(pSource); + + rc2 = RTCritSectLeave(&pSource->CritSect); + AssertRC(rc2); + } +} + +/** + * Generates a new event ID for a specific event source and registers it. + * + * @returns VBox status code. + * @param pSource Event source to generate event for. + * @param ppEvent Where to return the new event generated on success. + */ +int ShClEventSourceGenerateAndRegisterEvent(PSHCLEVENTSOURCE pSource, PSHCLEVENT *ppEvent) +{ + AssertPtrReturn(pSource, VERR_INVALID_POINTER); + AssertPtrReturn(ppEvent, VERR_INVALID_POINTER); + + PSHCLEVENT pEvent = (PSHCLEVENT)RTMemAllocZ(sizeof(SHCLEVENT)); + AssertReturn(pEvent, VERR_NO_MEMORY); + int rc = RTSemEventMultiCreate(&pEvent->hEvtMulSem); + if (RT_SUCCESS(rc)) + { + rc = RTCritSectEnter(&pSource->CritSect); + if (RT_SUCCESS(rc)) + { + /* + * Allocate an unique event ID. + */ + for (uint32_t cTries = 0;; cTries++) + { + SHCLEVENTID idEvent = ++pSource->idNextEvent; + if (idEvent < VBOX_SHCL_MAX_EVENTS) + { /* likely */ } + else + pSource->idNextEvent = idEvent = 1; /* zero == error, remember! */ + + if (shclEventGet(pSource, idEvent) == NULL) + { + pEvent->pParent = pSource; + pEvent->idEvent = idEvent; + RTListAppend(&pSource->lstEvents, &pEvent->Node); + + rc = RTCritSectLeave(&pSource->CritSect); + AssertRC(rc); + + LogFlowFunc(("uSource=%RU16: New event: %#x\n", pSource->uID, idEvent)); + + ShClEventRetain(pEvent); + *ppEvent = pEvent; + + return VINF_SUCCESS; + } + + AssertBreak(cTries < 4096); + } + + rc = RTCritSectLeave(&pSource->CritSect); + AssertRC(rc); + } + } + + AssertMsgFailed(("Unable to register a new event ID for event source %RU16\n", pSource->uID)); + + RTSemEventMultiDestroy(pEvent->hEvtMulSem); + pEvent->hEvtMulSem = NIL_RTSEMEVENTMULTI; + RTMemFree(pEvent); + return rc; +} + +/** + * Destroys an event. + * + * @param pEvent Event to destroy. + */ +static void shClEventDestroy(PSHCLEVENT pEvent) +{ + if (!pEvent) + return; + + AssertMsgReturnVoid(pEvent->cRefs == 0, ("Event %RU32 still has %RU32 references\n", + pEvent->idEvent, pEvent->cRefs)); + + LogFlowFunc(("Event %RU32\n", pEvent->idEvent)); + + if (pEvent->hEvtMulSem != NIL_RTSEMEVENT) + { + RTSemEventMultiDestroy(pEvent->hEvtMulSem); + pEvent->hEvtMulSem = NIL_RTSEMEVENT; + } + + ShClPayloadFree(pEvent->pPayload); + + pEvent->idEvent = NIL_SHCLEVENTID; +} + +/** + * Unregisters an event. + * + * @returns VBox status code. + * @param pSource Event source to unregister event for. + * @param pEvent Event to unregister. On success the pointer will be invalid. + */ +static int shClEventSourceUnregisterEventInternal(PSHCLEVENTSOURCE pSource, PSHCLEVENT pEvent) +{ + LogFlowFunc(("idEvent=%RU32, cRefs=%RU32\n", pEvent->idEvent, pEvent->cRefs)); + + AssertReturn(pEvent->cRefs == 0, VERR_WRONG_ORDER); + + int rc = RTCritSectEnter(&pSource->CritSect); + if (RT_SUCCESS(rc)) + { + RTListNodeRemove(&pEvent->Node); + + shClEventDestroy(pEvent); + + rc = RTCritSectLeave(&pSource->CritSect); + if (RT_SUCCESS(rc)) + { + RTMemFree(pEvent); + pEvent = NULL; + } + } + + return rc; +} + +/** + * Returns a specific event of a event source. Inlined version. + * + * @returns Pointer to event if found, or NULL if not found. + * @param pSource Event source to get event from. + * @param uID Event ID to get. + */ +DECLINLINE(PSHCLEVENT) shclEventGet(PSHCLEVENTSOURCE pSource, SHCLEVENTID idEvent) +{ + PSHCLEVENT pEvent; + RTListForEach(&pSource->lstEvents, pEvent, SHCLEVENT, Node) + { + if (pEvent->idEvent == idEvent) + return pEvent; + } + + return NULL; +} + +/** + * Returns a specific event of a event source. + * + * @returns Pointer to event if found, or NULL if not found. + * @param pSource Event source to get event from. + * @param idEvent ID of event to return. + */ +PSHCLEVENT ShClEventSourceGetFromId(PSHCLEVENTSOURCE pSource, SHCLEVENTID idEvent) +{ + AssertPtrReturn(pSource, NULL); + + int rc = RTCritSectEnter(&pSource->CritSect); + if (RT_SUCCESS(rc)) + { + PSHCLEVENT pEvent = shclEventGet(pSource, idEvent); + + rc = RTCritSectLeave(&pSource->CritSect); + AssertRC(rc); + + return pEvent; + } + + return NULL; +} + +/** + * Returns the last (newest) event ID which has been registered for an event source. + * + * @returns Pointer to last registered event, or NULL if not found. + * @param pSource Event source to get last registered event from. + */ +PSHCLEVENT ShClEventSourceGetLast(PSHCLEVENTSOURCE pSource) +{ + AssertPtrReturn(pSource, NULL); + + int rc = RTCritSectEnter(&pSource->CritSect); + if (RT_SUCCESS(rc)) + { + PSHCLEVENT pEvent = RTListGetLast(&pSource->lstEvents, SHCLEVENT, Node); + + rc = RTCritSectLeave(&pSource->CritSect); + AssertRC(rc); + + return pEvent; + } + + return NULL; +} + +/** + * Returns the current reference count for a specific event. + * + * @returns Reference count. + * @param pSource Event source the specific event is part of. + * @param idEvent Event ID to return reference count for. + */ +uint32_t ShClEventGetRefs(PSHCLEVENT pEvent) +{ + AssertPtrReturn(pEvent, 0); + + return ASMAtomicReadU32(&pEvent->cRefs); +} + +/** + * Detaches a payload from an event, internal version. + * + * @returns Pointer to the detached payload. Can be NULL if the payload has no payload. + * @param pEvent Event to detach payload for. + */ +static PSHCLEVENTPAYLOAD shclEventPayloadDetachInternal(PSHCLEVENT pEvent) +{ +#ifdef VBOX_STRICT + AssertPtrReturn(pEvent, NULL); +#endif + + PSHCLEVENTPAYLOAD pPayload = pEvent->pPayload; + + pEvent->pPayload = NULL; + + return pPayload; +} + +/** + * Waits for an event to get signalled. + * + * @returns VBox status code. + * @param pEvent Event to wait for. + * @param uTimeoutMs Timeout (in ms) to wait. + * @param ppPayload Where to store the (allocated) event payload on success. Needs to be free'd with + * SharedClipboardPayloadFree(). Optional. + */ +int ShClEventWait(PSHCLEVENT pEvent, RTMSINTERVAL uTimeoutMs, PSHCLEVENTPAYLOAD *ppPayload) +{ + AssertPtrReturn(pEvent, VERR_INVALID_POINTER); + AssertPtrNullReturn(ppPayload, VERR_INVALID_POINTER); + LogFlowFuncEnter(); + + int rc = RTSemEventMultiWait(pEvent->hEvtMulSem, uTimeoutMs); + if (RT_SUCCESS(rc)) + { + if (ppPayload) + { + /* Make sure to detach payload here, as the caller now owns the data. */ + *ppPayload = shclEventPayloadDetachInternal(pEvent); + } + } + + if (RT_FAILURE(rc)) + LogRel2(("Shared Clipboard: Waiting for event %RU32 failed, rc=%Rrc\n", pEvent->idEvent, rc)); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Retains an event by increasing its reference count. + * + * @returns New reference count, or UINT32_MAX if failed. + * @param pEvent Event to retain. + */ +uint32_t ShClEventRetain(PSHCLEVENT pEvent) +{ + AssertPtrReturn(pEvent, UINT32_MAX); + AssertReturn(ASMAtomicReadU32(&pEvent->cRefs) < 64, UINT32_MAX); + return ASMAtomicIncU32(&pEvent->cRefs); +} + +/** + * Releases an event by decreasing its reference count. + * + * @returns New reference count, or UINT32_MAX if failed. + * @param pEvent Event to release. + * If the reference count reaches 0, the event will + * be destroyed and \a pEvent will be invalid. + */ +uint32_t ShClEventRelease(PSHCLEVENT pEvent) +{ + if (!pEvent) + return 0; + + AssertReturn(ASMAtomicReadU32(&pEvent->cRefs) > 0, UINT32_MAX); + + uint32_t const cRefs = ASMAtomicDecU32(&pEvent->cRefs); + if (cRefs == 0) + { + AssertPtr(pEvent->pParent); + int rc2 = shClEventSourceUnregisterEventInternal(pEvent->pParent, pEvent); + AssertRC(rc2); + + return RT_SUCCESS(rc2) ? 0 : UINT32_MAX; + } + + return cRefs; +} + +/** + * Signals an event. + * + * @returns VBox status code. + * @param pEvent Event to signal. + * @param pPayload Event payload to associate. Takes ownership on + * success. Optional. + */ +int ShClEventSignal(PSHCLEVENT pEvent, PSHCLEVENTPAYLOAD pPayload) +{ + AssertPtrReturn(pEvent, VERR_INVALID_POINTER); + + Assert(pEvent->pPayload == NULL); + + pEvent->pPayload = pPayload; + + int rc = RTSemEventMultiSignal(pEvent->hEvtMulSem); + if (RT_FAILURE(rc)) + pEvent->pPayload = NULL; /* (no race condition if consumer also enters the critical section) */ + + LogFlowFuncLeaveRC(rc); + return rc; +} + +int ShClUtf16LenUtf8(PCRTUTF16 pcwszSrc, size_t cwcSrc, size_t *pchLen) +{ + AssertPtrReturn(pcwszSrc, VERR_INVALID_POINTER); + AssertPtrReturn(pchLen, VERR_INVALID_POINTER); + + size_t chLen = 0; + int rc = RTUtf16CalcUtf8LenEx(pcwszSrc, cwcSrc, &chLen); + if (RT_SUCCESS(rc)) + *pchLen = chLen; + return rc; +} + +int ShClConvUtf16CRLFToUtf8LF(PCRTUTF16 pcwszSrc, size_t cwcSrc, + char *pszBuf, size_t cbBuf, size_t *pcbLen) +{ + AssertPtrReturn(pcwszSrc, VERR_INVALID_POINTER); + AssertReturn (cwcSrc, VERR_INVALID_PARAMETER); + AssertPtrReturn(pszBuf, VERR_INVALID_POINTER); + AssertPtrReturn(pcbLen, VERR_INVALID_POINTER); + + int rc; + + PRTUTF16 pwszTmp = NULL; + size_t cchTmp = 0; + + size_t cbLen = 0; + + /* How long will the converted text be? */ + rc = ShClUtf16CRLFLenUtf8(pcwszSrc, cwcSrc, &cchTmp); + if (RT_SUCCESS(rc)) + { + cchTmp++; /* Add space for terminator. */ + + pwszTmp = (PRTUTF16)RTMemAlloc(cchTmp * sizeof(RTUTF16)); + if (pwszTmp) + { + rc = ShClConvUtf16CRLFToLF(pcwszSrc, cwcSrc, pwszTmp, cchTmp); + if (RT_SUCCESS(rc)) + rc = RTUtf16ToUtf8Ex(pwszTmp + 1, cchTmp - 1, &pszBuf, cbBuf, &cbLen); + + RTMemFree(reinterpret_cast(pwszTmp)); + } + else + rc = VERR_NO_MEMORY; + } + + if (RT_SUCCESS(rc)) + { + *pcbLen = cbLen; + } + + return rc; +} + +int ShClConvUtf16LFToCRLFA(PCRTUTF16 pcwszSrc, size_t cwcSrc, + PRTUTF16 *ppwszDst, size_t *pcwDst) +{ + AssertPtrReturn(pcwszSrc, VERR_INVALID_POINTER); + AssertPtrReturn(ppwszDst, VERR_INVALID_POINTER); + AssertPtrReturn(pcwDst, VERR_INVALID_POINTER); + + PRTUTF16 pwszDst = NULL; + size_t cchDst; + + int rc = ShClUtf16LFLenUtf8(pcwszSrc, cwcSrc, &cchDst); + if (RT_SUCCESS(rc)) + { + pwszDst = (PRTUTF16)RTMemAlloc((cchDst + 1 /* Leave space for terminator */) * sizeof(RTUTF16)); + if (pwszDst) + { + rc = ShClConvUtf16LFToCRLF(pcwszSrc, cwcSrc, pwszDst, cchDst + 1 /* Include terminator */); + } + else + rc = VERR_NO_MEMORY; + } + + if (RT_SUCCESS(rc)) + { + *ppwszDst = pwszDst; + *pcwDst = cchDst; + } + else + RTMemFree(pwszDst); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +int ShClConvUtf8LFToUtf16CRLF(const char *pcszSrc, size_t cbSrc, + PRTUTF16 *ppwszDst, size_t *pcwDst) +{ + AssertPtrReturn(pcszSrc, VERR_INVALID_POINTER); + AssertReturn(cbSrc, VERR_INVALID_PARAMETER); + AssertPtrReturn(ppwszDst, VERR_INVALID_POINTER); + AssertPtrReturn(pcwDst, VERR_INVALID_POINTER); + + /* Intermediate conversion to UTF-16. */ + size_t cwcTmp; + PRTUTF16 pwcTmp = NULL; + int rc = RTStrToUtf16Ex(pcszSrc, cbSrc, &pwcTmp, 0, &cwcTmp); + if (RT_SUCCESS(rc)) + { + rc = ShClConvUtf16LFToCRLFA(pwcTmp, cwcTmp, ppwszDst, pcwDst); + RTUtf16Free(pwcTmp); + } + + return rc; +} + +/** + * Converts a Latin-1 string with LF line endings into an UTF-16 string with CRLF endings. + * + * @returns VBox status code. + * @param pcszSrc Latin-1 string to convert. + * @param cbSrc Size (in bytes) of Latin-1 string to convert. + * @param ppwszDst Where to return the converted UTF-16 string on success. + * @param pcwDst Where to return the length (in UTF-16 characters) on success. + * + * @note Only converts the source until the string terminator is found (or length limit is hit). + */ +int ShClConvLatin1LFToUtf16CRLF(const char *pcszSrc, size_t cbSrc, + PRTUTF16 *ppwszDst, size_t *pcwDst) +{ + AssertPtrReturn(pcszSrc, VERR_INVALID_POINTER); + AssertReturn(cbSrc, VERR_INVALID_PARAMETER); + AssertPtrReturn(ppwszDst, VERR_INVALID_POINTER); + AssertPtrReturn(pcwDst, VERR_INVALID_POINTER); + + size_t chSrc = 0; + + PRTUTF16 pwszDst = NULL; + + /* Calculate the space needed. */ + size_t cwDst = 0; + for (size_t i = 0; i < cbSrc && pcszSrc[i] != '\0'; ++i) + { + if (pcszSrc[i] == VBOX_SHCL_LINEFEED) + cwDst += 2; /* Space for VBOX_SHCL_CARRIAGERETURN + VBOX_SHCL_LINEFEED. */ + else + ++cwDst; + chSrc++; + } + + pwszDst = (PRTUTF16)RTMemAlloc((cwDst + 1 /* Leave space for the terminator */) * sizeof(RTUTF16)); + AssertPtrReturn(pwszDst, VERR_NO_MEMORY); + + /* Do the conversion, bearing in mind that Latin-1 expands "naturally" to UTF-16. */ + for (size_t i = 0, j = 0; i < chSrc; ++i, ++j) + { + AssertMsg(j <= cwDst, ("cbSrc=%zu, j=%u vs. cwDst=%u\n", cbSrc, j, cwDst)); + if (pcszSrc[i] != VBOX_SHCL_LINEFEED) + pwszDst[j] = pcszSrc[i]; + else + { + pwszDst[j] = VBOX_SHCL_CARRIAGERETURN; + pwszDst[j + 1] = VBOX_SHCL_LINEFEED; + ++j; + } + } + + pwszDst[cwDst] = '\0'; /* Make sure we are zero-terminated. */ + + *ppwszDst = pwszDst; + *pcwDst = cwDst; + + return VINF_SUCCESS; +} + +int ShClConvUtf16ToUtf8HTML(PCRTUTF16 pcwszSrc, size_t cwcSrc, char **ppszDst, size_t *pcbDst) +{ + AssertPtrReturn(pcwszSrc, VERR_INVALID_POINTER); + AssertReturn (cwcSrc, VERR_INVALID_PARAMETER); + AssertPtrReturn(ppszDst, VERR_INVALID_POINTER); + AssertPtrReturn(pcbDst, VERR_INVALID_POINTER); + + int rc = VINF_SUCCESS; + + size_t cwTmp = cwcSrc; + PCRTUTF16 pwTmp = pcwszSrc; + + char *pchDst = NULL; + size_t cbDst = 0; + + size_t i = 0; + while (i < cwTmp) + { + /* Find zero symbol (end of string). */ + for (; i < cwTmp && pcwszSrc[i] != 0; i++) + ; + + /* Convert found string. */ + char *psz = NULL; + size_t cch = 0; + rc = RTUtf16ToUtf8Ex(pwTmp, cwTmp, &psz, pwTmp - pcwszSrc, &cch); + if (RT_FAILURE(rc)) + break; + + /* Append new substring. */ + char *pchNew = (char *)RTMemRealloc(pchDst, cbDst + cch + 1); + if (!pchNew) + { + RTStrFree(psz); + rc = VERR_NO_MEMORY; + break; + } + + pchDst = pchNew; + memcpy(pchDst + cbDst, psz, cch + 1); + + RTStrFree(psz); + + cbDst += cch + 1; + + /* Skip zero symbols. */ + for (; i < cwTmp && pcwszSrc[i] == 0; i++) + ; + + /* Remember start of string. */ + pwTmp += i; + } + + if (RT_SUCCESS(rc)) + { + *ppszDst = pchDst; + *pcbDst = cbDst; + + return VINF_SUCCESS; + } + + RTMemFree(pchDst); + + return rc; +} + +int ShClUtf16LFLenUtf8(PCRTUTF16 pcwszSrc, size_t cwSrc, size_t *pchLen) +{ + AssertPtrReturn(pcwszSrc, VERR_INVALID_POINTER); + AssertPtrReturn(pchLen, VERR_INVALID_POINTER); + + AssertMsgReturn(pcwszSrc[0] != VBOX_SHCL_UTF16BEMARKER, + ("Big endian UTF-16 not supported yet\n"), VERR_NOT_SUPPORTED); + + size_t cLen = 0; + + /* Don't copy the endian marker. */ + size_t i = pcwszSrc[0] == VBOX_SHCL_UTF16LEMARKER ? 1 : 0; + + /* Calculate the size of the destination text string. */ + /* Is this Utf16 or Utf16-LE? */ + for (; i < cwSrc; ++i, ++cLen) + { + /* Check for a single line feed */ + if (pcwszSrc[i] == VBOX_SHCL_LINEFEED) + ++cLen; +#ifdef RT_OS_DARWIN + /* Check for a single carriage return (MacOS) */ + if (pcwszSrc[i] == VBOX_SHCL_CARRIAGERETURN) + ++cLen; +#endif + if (pcwszSrc[i] == 0) + { + /* Don't count this, as we do so below. */ + break; + } + } + + *pchLen = cLen; + + return VINF_SUCCESS; +} + +int ShClUtf16CRLFLenUtf8(PCRTUTF16 pcwszSrc, size_t cwSrc, size_t *pchLen) +{ + AssertPtrReturn(pcwszSrc, VERR_INVALID_POINTER); + AssertReturn(cwSrc, VERR_INVALID_PARAMETER); + AssertPtrReturn(pchLen, VERR_INVALID_POINTER); + + AssertMsgReturn(pcwszSrc[0] != VBOX_SHCL_UTF16BEMARKER, + ("Big endian UTF-16 not supported yet\n"), VERR_NOT_SUPPORTED); + + size_t cLen = 0; + + /* Calculate the size of the destination text string. */ + /* Is this Utf16 or Utf16-LE? */ + if (pcwszSrc[0] == VBOX_SHCL_UTF16LEMARKER) + cLen = 0; + else + cLen = 1; + + for (size_t i = 0; i < cwSrc; ++i, ++cLen) + { + if ( (i + 1 < cwSrc) + && (pcwszSrc[i] == VBOX_SHCL_CARRIAGERETURN) + && (pcwszSrc[i + 1] == VBOX_SHCL_LINEFEED)) + { + ++i; + } + if (pcwszSrc[i] == 0) + break; + } + + *pchLen = cLen; + + return VINF_SUCCESS; +} + +int ShClConvUtf16LFToCRLF(PCRTUTF16 pcwszSrc, size_t cwcSrc, PRTUTF16 pu16Dst, size_t cwDst) +{ + AssertPtrReturn(pcwszSrc, VERR_INVALID_POINTER); + AssertPtrReturn(pu16Dst, VERR_INVALID_POINTER); + AssertReturn(cwDst, VERR_INVALID_PARAMETER); + + AssertMsgReturn(pcwszSrc[0] != VBOX_SHCL_UTF16BEMARKER, + ("Big endian UTF-16 not supported yet\n"), VERR_NOT_SUPPORTED); + + int rc = VINF_SUCCESS; + + /* Don't copy the endian marker. */ + size_t i = pcwszSrc[0] == VBOX_SHCL_UTF16LEMARKER ? 1 : 0; + size_t j = 0; + + for (; i < cwcSrc; ++i, ++j) + { + /* Don't copy the null byte, as we add it below. */ + if (pcwszSrc[i] == 0) + break; + + /* Not enough space in destination? */ + if (j == cwDst) + { + rc = VERR_BUFFER_OVERFLOW; + break; + } + + if (pcwszSrc[i] == VBOX_SHCL_LINEFEED) + { + pu16Dst[j] = VBOX_SHCL_CARRIAGERETURN; + ++j; + + /* Not enough space in destination? */ + if (j == cwDst) + { + rc = VERR_BUFFER_OVERFLOW; + break; + } + } +#ifdef RT_OS_DARWIN + /* Check for a single carriage return (MacOS) */ + else if (pcwszSrc[i] == VBOX_SHCL_CARRIAGERETURN) + { + /* Set CR.r */ + pu16Dst[j] = VBOX_SHCL_CARRIAGERETURN; + ++j; + + /* Not enough space in destination? */ + if (j == cwDst) + { + rc = VERR_BUFFER_OVERFLOW; + break; + } + + /* Add line feed. */ + pu16Dst[j] = VBOX_SHCL_LINEFEED; + continue; + } +#endif + pu16Dst[j] = pcwszSrc[i]; + } + + if (j == cwDst) + rc = VERR_BUFFER_OVERFLOW; + + if (RT_SUCCESS(rc)) + { + /* Add terminator. */ + pu16Dst[j] = 0; + } + + return rc; +} + +int ShClConvUtf16CRLFToLF(PCRTUTF16 pcwszSrc, size_t cwcSrc, PRTUTF16 pu16Dst, size_t cwDst) +{ + AssertPtrReturn(pcwszSrc, VERR_INVALID_POINTER); + AssertReturn(cwcSrc, VERR_INVALID_PARAMETER); + AssertPtrReturn(pu16Dst, VERR_INVALID_POINTER); + AssertReturn(cwDst, VERR_INVALID_PARAMETER); + + AssertMsgReturn(pcwszSrc[0] != VBOX_SHCL_UTF16BEMARKER, + ("Big endian UTF-16 not supported yet\n"), VERR_NOT_SUPPORTED); + + /* Prepend the Utf16 byte order marker if it is missing. */ + size_t cwDstPos; + if (pcwszSrc[0] == VBOX_SHCL_UTF16LEMARKER) + { + cwDstPos = 0; + } + else + { + pu16Dst[0] = VBOX_SHCL_UTF16LEMARKER; + cwDstPos = 1; + } + + for (size_t i = 0; i < cwcSrc; ++i, ++cwDstPos) + { + if (pcwszSrc[i] == 0) + break; + + if (cwDstPos == cwDst) + return VERR_BUFFER_OVERFLOW; + + if ( (i + 1 < cwcSrc) + && (pcwszSrc[i] == VBOX_SHCL_CARRIAGERETURN) + && (pcwszSrc[i + 1] == VBOX_SHCL_LINEFEED)) + { + ++i; + } + + pu16Dst[cwDstPos] = pcwszSrc[i]; + } + + if (cwDstPos == cwDst) + return VERR_BUFFER_OVERFLOW; + + /* Add terminating zero. */ + pu16Dst[cwDstPos] = 0; + + return VINF_SUCCESS; +} + +int ShClDibToBmp(const void *pvSrc, size_t cbSrc, void **ppvDest, size_t *pcbDest) +{ + AssertPtrReturn(pvSrc, VERR_INVALID_POINTER); + AssertReturn(cbSrc, VERR_INVALID_PARAMETER); + AssertPtrReturn(ppvDest, VERR_INVALID_POINTER); + AssertPtrReturn(pcbDest, VERR_INVALID_POINTER); + + PBMPWIN3XINFOHDR coreHdr = (PBMPWIN3XINFOHDR)pvSrc; + /** @todo Support all the many versions of the DIB headers. */ + if ( cbSrc < sizeof(BMPWIN3XINFOHDR) + || RT_LE2H_U32(coreHdr->cbSize) < sizeof(BMPWIN3XINFOHDR) + || RT_LE2H_U32(coreHdr->cbSize) != sizeof(BMPWIN3XINFOHDR)) + { + return VERR_INVALID_PARAMETER; + } + + size_t offPixel = sizeof(BMPFILEHDR) + + RT_LE2H_U32(coreHdr->cbSize) + + RT_LE2H_U32(coreHdr->cClrUsed) * sizeof(uint32_t); + if (cbSrc < offPixel) + return VERR_INVALID_PARAMETER; + + size_t cbDst = sizeof(BMPFILEHDR) + cbSrc; + + void *pvDest = RTMemAlloc(cbDst); + if (!pvDest) + return VERR_NO_MEMORY; + + PBMPFILEHDR fileHdr = (PBMPFILEHDR)pvDest; + + fileHdr->uType = BMP_HDR_MAGIC; + fileHdr->cbFileSize = (uint32_t)RT_H2LE_U32(cbDst); + fileHdr->Reserved1 = 0; + fileHdr->Reserved2 = 0; + fileHdr->offBits = (uint32_t)RT_H2LE_U32(offPixel); + + memcpy((uint8_t *)pvDest + sizeof(BMPFILEHDR), pvSrc, cbSrc); + + *ppvDest = pvDest; + *pcbDest = cbDst; + + return VINF_SUCCESS; +} + +int ShClBmpGetDib(const void *pvSrc, size_t cbSrc, const void **ppvDest, size_t *pcbDest) +{ + AssertPtrReturn(pvSrc, VERR_INVALID_POINTER); + AssertReturn(cbSrc, VERR_INVALID_PARAMETER); + AssertPtrReturn(ppvDest, VERR_INVALID_POINTER); + AssertPtrReturn(pcbDest, VERR_INVALID_POINTER); + + PBMPFILEHDR pBmpHdr = (PBMPFILEHDR)pvSrc; + if ( cbSrc < sizeof(BMPFILEHDR) + || pBmpHdr->uType != BMP_HDR_MAGIC + || RT_LE2H_U32(pBmpHdr->cbFileSize) != cbSrc) + { + return VERR_INVALID_PARAMETER; + } + + *ppvDest = ((uint8_t *)pvSrc) + sizeof(BMPFILEHDR); + *pcbDest = cbSrc - sizeof(BMPFILEHDR); + + return VINF_SUCCESS; +} + +#ifdef LOG_ENABLED + +int ShClDbgDumpHtml(const char *pcszSrc, size_t cbSrc) +{ + int rc = VINF_SUCCESS; + char *pszBuf = (char *)RTMemTmpAllocZ(cbSrc + 1); + if (pszBuf) + { + memcpy(pszBuf, pcszSrc, cbSrc); + pszBuf[cbSrc] = '\0'; + for (size_t off = 0; off < cbSrc; ++off) + if (pszBuf[off] == '\n' || pszBuf[off] == '\r') + pszBuf[off] = ' '; + LogFunc(("Removed \\r\\n: %s\n", pszBuf)); + RTMemTmpFree(pszBuf); + } + else + rc = VERR_NO_MEMORY; + return rc; +} + +void ShClDbgDumpData(const void *pv, size_t cb, SHCLFORMAT uFormat) +{ + if (LogIsEnabled()) + { + if (uFormat & VBOX_SHCL_FMT_UNICODETEXT) + { + LogFunc(("VBOX_SHCL_FMT_UNICODETEXT:\n")); + if (pv && cb) + LogFunc(("%ls\n", pv)); + else + LogFunc(("%p %zu\n", pv, cb)); + } + else if (uFormat & VBOX_SHCL_FMT_BITMAP) + LogFunc(("VBOX_SHCL_FMT_BITMAP\n")); + else if (uFormat & VBOX_SHCL_FMT_HTML) + { + LogFunc(("VBOX_SHCL_FMT_HTML:\n")); + if (pv && cb) + { + LogFunc(("%s\n", pv)); + ShClDbgDumpHtml((const char *)pv, cb); + } + else + LogFunc(("%p %zu\n", pv, cb)); + } + else + LogFunc(("Invalid format %02X\n", uFormat)); + } +} + +#endif /* LOG_ENABLED */ + +/** + * Translates a Shared Clipboard host function number to a string. + * + * @returns Function ID string name. + * @param uFn The function to translate. + */ +const char *ShClHostFunctionToStr(uint32_t uFn) +{ + switch (uFn) + { + RT_CASE_RET_STR(VBOX_SHCL_HOST_FN_SET_MODE); + RT_CASE_RET_STR(VBOX_SHCL_HOST_FN_SET_TRANSFER_MODE); + RT_CASE_RET_STR(VBOX_SHCL_HOST_FN_SET_HEADLESS); + RT_CASE_RET_STR(VBOX_SHCL_HOST_FN_CANCEL); + RT_CASE_RET_STR(VBOX_SHCL_HOST_FN_ERROR); + } + return "Unknown"; +} + +/** + * Translates a Shared Clipboard host message enum to a string. + * + * @returns Message ID string name. + * @param uMsg The message to translate. + */ +const char *ShClHostMsgToStr(uint32_t uMsg) +{ + switch (uMsg) + { + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_QUIT); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_READ_DATA); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_FORMATS_REPORT); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_CANCELED); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_READ_DATA_CID); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_STATUS); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_ROOT_LIST_HDR_READ); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_ROOT_LIST_HDR_WRITE); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_ROOT_LIST_ENTRY_READ); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_ROOT_LIST_ENTRY_WRITE); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_LIST_OPEN); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_LIST_CLOSE); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_LIST_HDR_READ); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_LIST_HDR_WRITE); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_LIST_ENTRY_READ); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_LIST_ENTRY_WRITE); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_OBJ_OPEN); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_OBJ_CLOSE); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_OBJ_READ); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_OBJ_WRITE); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_CANCEL); + RT_CASE_RET_STR(VBOX_SHCL_HOST_MSG_TRANSFER_ERROR); + } + return "Unknown"; +} + +/** + * Translates a Shared Clipboard guest message enum to a string. + * + * @returns Message ID string name. + * @param uMsg The message to translate. + */ +const char *ShClGuestMsgToStr(uint32_t uMsg) +{ + switch (uMsg) + { + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_MSG_OLD_GET_WAIT); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_REPORT_FORMATS); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_DATA_READ); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_DATA_WRITE); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_CONNECT); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_REPORT_FEATURES); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_QUERY_FEATURES); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_MSG_PEEK_NOWAIT); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_MSG_PEEK_WAIT); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_MSG_GET); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_MSG_CANCEL); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_REPLY); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_ROOT_LIST_HDR_READ); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_ROOT_LIST_HDR_WRITE); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_ROOT_LIST_ENTRY_READ); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_ROOT_LIST_ENTRY_WRITE); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_LIST_OPEN); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_LIST_CLOSE); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_LIST_HDR_READ); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_LIST_HDR_WRITE); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_LIST_ENTRY_READ); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_LIST_ENTRY_WRITE); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_OBJ_OPEN); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_OBJ_CLOSE); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_OBJ_READ); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_OBJ_WRITE); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_ERROR); + RT_CASE_RET_STR(VBOX_SHCL_GUEST_FN_NEGOTIATE_CHUNK_SIZE); + } + return "Unknown"; +} + +/** + * Converts Shared Clipboard formats to a string. + * + * @returns Stringified Shared Clipboard formats, or NULL on failure. Must be free'd with RTStrFree(). + * @param fFormats Shared Clipboard formats to convert. + * + */ +char *ShClFormatsToStrA(SHCLFORMATS fFormats) +{ +#define APPEND_FMT_TO_STR(_aFmt) \ + if (fFormats & VBOX_SHCL_FMT_##_aFmt) \ + { \ + if (pszFmts) \ + { \ + rc2 = RTStrAAppend(&pszFmts, ", "); \ + if (RT_FAILURE(rc2)) \ + break; \ + } \ + \ + rc2 = RTStrAAppend(&pszFmts, #_aFmt); \ + if (RT_FAILURE(rc2)) \ + break; \ + } + + char *pszFmts = NULL; + int rc2 = VINF_SUCCESS; + + do + { + APPEND_FMT_TO_STR(UNICODETEXT); + APPEND_FMT_TO_STR(BITMAP); + APPEND_FMT_TO_STR(HTML); +# ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + APPEND_FMT_TO_STR(URI_LIST); +# endif + + } while (0); + + if (!pszFmts) + rc2 = RTStrAAppend(&pszFmts, "NONE"); + + if ( RT_FAILURE(rc2) + && pszFmts) + { + RTStrFree(pszFmts); + pszFmts = NULL; + } + +#undef APPEND_FMT_TO_STR + + return pszFmts; +} + diff --git a/src/VBox/GuestHost/SharedClipboard/clipboard-transfers-http.cpp b/src/VBox/GuestHost/SharedClipboard/clipboard-transfers-http.cpp new file mode 100644 index 00000000..71504151 --- /dev/null +++ b/src/VBox/GuestHost/SharedClipboard/clipboard-transfers-http.cpp @@ -0,0 +1,862 @@ +/* $Id: clipboard-transfers-http.cpp $ */ +/** @file + * Shared Clipboard: HTTP server implementation for Shared Clipboard transfers on UNIX-y guests / hosts. + */ + +/* + * Copyright (C) 2020-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include + +#include +#include + +#include /* To make use of IPv4Addr in RTGETOPTUNION. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#define LOG_GROUP LOG_GROUP_SHARED_CLIPBOARD +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + +/********************************************************************************************************************************* +* Definitations * +*********************************************************************************************************************************/ + +typedef struct _SHCLHTTPSERVERTRANSFER +{ + /** The node list. */ + RTLISTNODE Node; + /** Pointer to associated transfer. */ + PSHCLTRANSFER pTransfer; + /** The (cached) root list of the transfer. NULL if not cached yet. */ + PSHCLROOTLIST pRootList; + /** Critical section for serializing access. */ + RTCRITSECT CritSect; + /** The handle we're going to use for this HTTP transfer. */ + SHCLOBJHANDLE hObj; + /** The virtual path of the HTTP server's root directory for this transfer. */ + char szPathVirtual[RTPATH_MAX]; +} SHCLHTTPSERVERTRANSFER; +typedef SHCLHTTPSERVERTRANSFER *PSHCLHTTPSERVERTRANSFER; + + +/********************************************************************************************************************************* +* Prototypes * +*********************************************************************************************************************************/ +static int shClTransferHttpServerDestroyInternal(PSHCLHTTPSERVER pThis); +static const char *shClTransferHttpServerGetHost(PSHCLHTTPSERVER pSrv); + + +/********************************************************************************************************************************* +* Public Shared Clipboard HTTP transfer functions * +*********************************************************************************************************************************/ + +/** + * Registers a Shared Clipboard transfer to a HTTP context. + * + * @returns VBox status code. + * @param pCtx HTTP context to register transfer for. + * @param pTransfer Transfer to register. + */ +int ShClHttpTransferRegister(PSHCLHTTPCONTEXT pCtx, PSHCLTRANSFER pTransfer) +{ + int rc = VINF_SUCCESS; + + /* Start the built-in HTTP server to serve file(s). */ + if (!ShClTransferHttpServerIsRunning(&pCtx->HttpServer)) /* Only one HTTP server per transfer context. */ + rc = ShClTransferHttpServerCreate(&pCtx->HttpServer, NULL /* puPort */); + + if (RT_SUCCESS(rc)) + rc = ShClTransferHttpServerRegisterTransfer(&pCtx->HttpServer, pTransfer); + + return rc; +} + +/** + * Unregisters a formerly registered Shared Clipboard transfer. + * + * @returns VBox status code. + * @param pCtx HTTP context to unregister transfer from. + * @param pTransfer Transfer to unregister. + */ +int ShClHttpTransferUnregister(PSHCLHTTPCONTEXT pCtx, PSHCLTRANSFER pTransfer) +{ + int rc = VINF_SUCCESS; + + if (ShClTransferHttpServerIsRunning(&pCtx->HttpServer)) + { + /* Try unregistering transfer (if it was registered before). */ + rc = ShClTransferHttpServerUnregisterTransfer(&pCtx->HttpServer, pTransfer); + if (RT_SUCCESS(rc)) + { + /* No more registered transfers left? Tear down the HTTP server instance then. */ + if (ShClTransferHttpServerGetTransferCount(&pCtx->HttpServer) == 0) + rc = ShClTransferHttpServerDestroy(&pCtx->HttpServer); + } + AssertRC(rc); + } + + return rc; +} + + +/********************************************************************************************************************************* +* Internal Shared Clipboard HTTP transfer functions * +*********************************************************************************************************************************/ + +DECLINLINE(void) shClHttpTransferLock(PSHCLHTTPSERVERTRANSFER pSrvTx) +{ + int rc2 = RTCritSectEnter(&pSrvTx->CritSect); + AssertRC(rc2); +} + +DECLINLINE(void) shClHttpTransferUnlock(PSHCLHTTPSERVERTRANSFER pSrvTx) +{ + int rc2 = RTCritSectEnter(&pSrvTx->CritSect); + AssertRC(rc2); +} + +/** + * Return the HTTP server transfer for a specific transfer ID. + * + * @returns Pointer to HTTP server transfer if found, NULL if not found. + * @param pSrv HTTP server instance. + * @param idTransfer Transfer ID to return HTTP server transfer for. + */ +static PSHCLHTTPSERVERTRANSFER shClTransferHttpServerGetTransferById(PSHCLHTTPSERVER pSrv, SHCLTRANSFERID idTransfer) +{ + PSHCLHTTPSERVERTRANSFER pSrvTx; + RTListForEach(&pSrv->lstTransfers, pSrvTx, SHCLHTTPSERVERTRANSFER, Node) /** @todo Slow O(n) lookup, but does it for now. */ + { + if (pSrvTx->pTransfer->State.uID == idTransfer) + return pSrvTx; + } + + return NULL; +} + +/** + * Returns a HTTP server transfer from a given URL. + * + * @returns Pointer to HTTP server transfer if found, NULL if not found. + * @param pThis HTTP server instance data. + * @param pszUrl URL to validate. + */ +DECLINLINE(PSHCLHTTPSERVERTRANSFER) shClTransferHttpGetTransferFromUrl(PSHCLHTTPSERVER pThis, const char *pszUrl) +{ + AssertPtrReturn(pszUrl, NULL); + + PSHCLHTTPSERVERTRANSFER pSrvTx = NULL; + + PSHCLHTTPSERVERTRANSFER pSrvTxCur; + RTListForEach(&pThis->lstTransfers, pSrvTxCur, SHCLHTTPSERVERTRANSFER, Node) + { + AssertPtr(pSrvTxCur->pTransfer); + + LogFlowFunc(("pSrvTxCur=%s\n", pSrvTxCur->szPathVirtual)); + + /* Be picky here, do a case sensitive comparison. */ + if (RTStrStartsWith(pszUrl, pSrvTxCur->szPathVirtual)) + { + pSrvTx = pSrvTxCur; + break; + } + } + + if (!pSrvTx) + LogRel2(("Shared Clipboard: HTTP URL '%s' not valid\n", pszUrl)); + + LogFlowFunc(("pszUrl=%s, pSrvTx=%p\n", pszUrl, pSrvTx)); + return pSrvTx; +} + +/** + * Returns a HTTP server transfer from an internal HTTP handle. + * + * @returns Pointer to HTTP server transfer if found, NULL if not found. + * @param pThis HTTP server instance data. + * @param pvHandle Handle to return transfer for. + */ +DECLINLINE(PSHCLHTTPSERVERTRANSFER) shClTransferHttpGetTransferFromHandle(PSHCLHTTPSERVER pThis, void *pvHandle) +{ + AssertPtrReturn(pvHandle, NULL); + + const SHCLTRANSFERID uHandle = *(uint16_t *)pvHandle; + + /** @ŧodo Use a handle lookup table (map) later. */ + PSHCLHTTPSERVERTRANSFER pSrvTxCur; + RTListForEach(&pThis->lstTransfers, pSrvTxCur, SHCLHTTPSERVERTRANSFER, Node) + { + AssertPtr(pSrvTxCur->pTransfer); + + if (pSrvTxCur->pTransfer->State.uID == uHandle) /** @ŧodo We're using the transfer ID as handle for now. */ + return pSrvTxCur; + } + + return NULL; +} + +static int shClTransferHttpGetTransferRoots(PSHCLHTTPSERVER pThis, PSHCLHTTPSERVERTRANSFER pSrvTx) +{ + RT_NOREF(pThis); + + int rc = VINF_SUCCESS; + + if (pSrvTx->pRootList == NULL) + { + AssertPtr(pSrvTx->pTransfer); + rc = ShClTransferRootsGet(pSrvTx->pTransfer, &pSrvTx->pRootList); + } + + return rc; +} + + +/********************************************************************************************************************************* +* HTTP server callback implementations * +*********************************************************************************************************************************/ + +/** @copydoc RTHTTPSERVERCALLBACKS::pfnOpen */ +static DECLCALLBACK(int) shClTransferHttpOpen(PRTHTTPCALLBACKDATA pData, PRTHTTPSERVERREQ pReq, void **ppvHandle) +{ + PSHCLHTTPSERVER pThis = (PSHCLHTTPSERVER)pData->pvUser; + Assert(pData->cbUser == sizeof(SHCLHTTPSERVER)); + + int rc; + + PSHCLHTTPSERVERTRANSFER pSrvTx = shClTransferHttpGetTransferFromUrl(pThis, pReq->pszUrl); + if (pSrvTx) + { + shClHttpTransferLock(pSrvTx); + + AssertPtr(pSrvTx->pTransfer); + + SHCLOBJOPENCREATEPARMS openParms; + rc = ShClTransferObjOpenParmsInit(&openParms); + if (RT_SUCCESS(rc)) + { + openParms.fCreate = SHCL_OBJ_CF_ACCESS_READ + | SHCL_OBJ_CF_ACCESS_DENYWRITE; + + rc = RTStrCopy(openParms.pszPath, openParms.cbPath, "foo"); /** @ŧodo BUGBUG !!!! */ + if (RT_SUCCESS(rc)) + { + rc = ShClTransferObjOpen(pSrvTx->pTransfer, &openParms, &pSrvTx->hObj); + if (RT_SUCCESS(rc)) + { + *ppvHandle = &pSrvTx->hObj; + LogRel2(("Shared Clipboard: HTTP transfer (handle %RU64) started ...\n", pSrvTx->hObj)); + } + } + + ShClTransferObjOpenParmsDestroy(&openParms); + } + + shClHttpTransferUnlock(pSrvTx); + } + else + rc = VERR_NOT_FOUND; + + if (RT_FAILURE(rc)) + LogRel(("Shared Clipboard: Error starting HTTP transfer for '%s', rc=%Rrc\n", pReq->pszUrl, rc)); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** @copydoc RTHTTPSERVERCALLBACKS::pfnRead */ +static DECLCALLBACK(int) shClTransferHttpRead(PRTHTTPCALLBACKDATA pData, void *pvHandle, void *pvBuf, size_t cbBuf, size_t *pcbRead) +{ + PSHCLHTTPSERVER pThis = (PSHCLHTTPSERVER)pData->pvUser; + Assert(pData->cbUser == sizeof(SHCLHTTPSERVER)); + + RT_NOREF(pvBuf, cbBuf, pcbRead); + + int rc; + + PSHCLHTTPSERVERTRANSFER pSrvTx = shClTransferHttpGetTransferFromHandle(pThis, pvHandle); + if (pSrvTx) + { + Assert(pSrvTx->hObj != SHCLOBJHANDLE_INVALID); + + uint32_t cbRead; + rc = ShClTransferObjRead(pSrvTx->pTransfer, pSrvTx->hObj, pvBuf, cbBuf, 0 /* fFlags */, &cbRead); + if (RT_SUCCESS(rc)) + { + *pcbRead = (uint32_t)cbRead; + } + + if (RT_FAILURE(rc)) + LogRel(("Shared Clipboard: Error reading HTTP transfer (handle %RU64), rc=%Rrc\n", pSrvTx->hObj, rc)); + } + else + rc = VERR_NOT_FOUND; + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** @copydoc RTHTTPSERVERCALLBACKS::pfnClose */ +static DECLCALLBACK(int) shClTransferHttpClose(PRTHTTPCALLBACKDATA pData, void *pvHandle) +{ + PSHCLHTTPSERVER pThis = (PSHCLHTTPSERVER)pData->pvUser; + Assert(pData->cbUser == sizeof(SHCLHTTPSERVER)); + + int rc; + + PSHCLHTTPSERVERTRANSFER pSrvTx = shClTransferHttpGetTransferFromHandle(pThis, pvHandle); + if (pSrvTx) + { + shClHttpTransferLock(pSrvTx); + + Assert(pSrvTx->hObj != SHCLOBJHANDLE_INVALID); + rc = ShClTransferObjClose(pSrvTx->pTransfer, pSrvTx->hObj); + if (RT_SUCCESS(rc)) + { + pSrvTx->hObj = SHCLOBJHANDLE_INVALID; + LogRel2(("Shared Clipboard: HTTP transfer %RU16 done\n", pSrvTx->pTransfer->State.uID)); + } + + if (RT_FAILURE(rc)) + LogRel(("Shared Clipboard: Error closing HTTP transfer (handle %RU64), rc=%Rrc\n", pSrvTx->hObj, rc)); + + shClHttpTransferUnlock(pSrvTx); + } + else + rc = VERR_NOT_FOUND; + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** @copydoc RTHTTPSERVERCALLBACKS::pfnQueryInfo */ +static DECLCALLBACK(int) shClTransferHttpQueryInfo(PRTHTTPCALLBACKDATA pData, + PRTHTTPSERVERREQ pReq, PRTFSOBJINFO pObjInfo, char **ppszMIMEHint) +{ + PSHCLHTTPSERVER pThis = (PSHCLHTTPSERVER)pData->pvUser; + Assert(pData->cbUser == sizeof(SHCLHTTPSERVER)); + + int rc; + + PSHCLHTTPSERVERTRANSFER pSrvTx = shClTransferHttpGetTransferFromUrl(pThis, pReq->pszUrl); + if (pSrvTx) + { + shClHttpTransferLock(pSrvTx); + + rc = shClTransferHttpGetTransferRoots(pThis, pSrvTx); + + shClHttpTransferUnlock(pSrvTx); + } + else + rc = VERR_NOT_FOUND; + + RT_NOREF(pObjInfo, ppszMIMEHint); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** @copydoc RTHTTPSERVERCALLBACKS::pfnDestroy */ +static DECLCALLBACK(int) shClTransferHttpDestroy(PRTHTTPCALLBACKDATA pData) +{ + PSHCLHTTPSERVER pThis = (PSHCLHTTPSERVER)pData->pvUser; + Assert(pData->cbUser == sizeof(SHCLHTTPSERVER)); + + return shClTransferHttpServerDestroyInternal(pThis); +} + + +/********************************************************************************************************************************* +* Internal Shared Clipboard HTTP server functions * +*********************************************************************************************************************************/ + +/** + * Destroys a Shared Clipboard HTTP server instance, internal version. + * + * @returns VBox status code. + * @param pSrv Shared Clipboard HTTP server instance to destroy. + */ +static int shClTransferHttpServerDestroyInternal(PSHCLHTTPSERVER pSrv) +{ + PSHCLHTTPSERVERTRANSFER pSrvTx, pSrvTxNext; + RTListForEachSafe(&pSrv->lstTransfers, pSrvTx, pSrvTxNext, SHCLHTTPSERVERTRANSFER, Node) + { + RTListNodeRemove(&pSrvTx->Node); + + RTMemFree(pSrvTx); + pSrvTx = NULL; + } + + RTHttpServerResponseDestroy(&pSrv->Resp); + + pSrv->hHTTPServer = NIL_RTHTTPSERVER; + + int rc = VINF_SUCCESS; + + if (RTCritSectIsInitialized(&pSrv->CritSect)) + rc = RTCritSectDelete(&pSrv->CritSect); + + return rc; +} + +/** + * Locks the critical section of a Shared Clipboard HTTP server instance. + * + * @param pSrv Shared Clipboard HTTP server instance to lock. + */ +DECLINLINE(void) shClTransferHttpServerLock(PSHCLHTTPSERVER pSrv) +{ + int rc2 = RTCritSectEnter(&pSrv->CritSect); + AssertRC(rc2); +} + +/** + * Unlocks the critical section of a Shared Clipboard HTTP server instance. + * + * @param pSrv Shared Clipboard HTTP server instance to unlock. + */ +DECLINLINE(void) shClTransferHttpServerUnlock(PSHCLHTTPSERVER pSrv) +{ + int rc2 = RTCritSectLeave(&pSrv->CritSect); + AssertRC(rc2); +} + +/** + * Initializes a new Shared Clipboard HTTP server instance. + * + * @param pSrv HTTP server instance to initialize. + */ +static void shClTransferHttpServerInitInternal(PSHCLHTTPSERVER pSrv) +{ + pSrv->hHTTPServer = NIL_RTHTTPSERVER; + pSrv->uPort = 0; + RTListInit(&pSrv->lstTransfers); + pSrv->cTransfers = 0; + int rc2 = RTHttpServerResponseInit(&pSrv->Resp); + AssertRC(rc2); +} + + +/********************************************************************************************************************************* +* Public Shared Clipboard HTTP server functions * +*********************************************************************************************************************************/ + +/** + * Initializes a new Shared Clipboard HTTP server instance. + * + * @param pSrv HTTP server instance to initialize. + */ +void ShClTransferHttpServerInit(PSHCLHTTPSERVER pSrv) +{ + AssertPtrReturnVoid(pSrv); + + shClTransferHttpServerInitInternal(pSrv); +} + +/** + * Creates a new Shared Clipboard HTTP server instance, extended version. + * + * @returns VBox status code. + * @param pSrv HTTP server instance to create. + * @param uPort TCP port number to use. + */ +int ShClTransferHttpServerCreateEx(PSHCLHTTPSERVER pSrv, uint16_t uPort) +{ + AssertPtrReturn(pSrv, VERR_INVALID_POINTER); + + RTHTTPSERVERCALLBACKS Callbacks; + RT_ZERO(Callbacks); + + Callbacks.pfnOpen = shClTransferHttpOpen; + Callbacks.pfnRead = shClTransferHttpRead; + Callbacks.pfnClose = shClTransferHttpClose; + Callbacks.pfnQueryInfo = shClTransferHttpQueryInfo; + Callbacks.pfnDestroy = shClTransferHttpDestroy; + + /* Note: The server always and *only* runs against the localhost interface. */ + int rc = RTHttpServerCreate(&pSrv->hHTTPServer, "localhost", uPort, &Callbacks, + pSrv, sizeof(SHCLHTTPSERVER)); + if (RT_SUCCESS(rc)) + { + rc = RTCritSectInit(&pSrv->CritSect); + AssertRCReturn(rc, rc); + + pSrv->uPort = uPort; + + LogRel2(("Shared Clipboard: HTTP server running at port %RU16\n", pSrv->uPort)); + } + else + { + int rc2 = shClTransferHttpServerDestroyInternal(pSrv); + AssertRC(rc2); + } + + if (RT_FAILURE(rc)) + LogRel(("Shared Clipboard: HTTP server failed to run, rc=%Rrc\n", rc)); + + return rc; +} + +/** + * Creates a new Shared Clipboard HTTP server instance. + * + * This does automatic probing of TCP ports if one already is being used. + * + * @returns VBox status code. + * @param pSrv HTTP server instance to create. + * @param puPort Where to return the TCP port number being used on success. Optional. + */ +int ShClTransferHttpServerCreate(PSHCLHTTPSERVER pSrv, uint16_t *puPort) +{ + AssertPtrReturn(pSrv, VERR_INVALID_POINTER); + /* puPort is optional. */ + + /** @todo Try favorite ports first (e.g. 8080, 8000, ...)? */ + + RTRAND hRand; + int rc = RTRandAdvCreateSystemFaster(&hRand); /* Should be good enough for this task. */ + if (RT_SUCCESS(rc)) + { + uint16_t uPort; + for (int i = 0; i < 32; i++) + { +#ifdef DEBUG_andy + uPort = 8080; /* Make the port predictable, but only for me, mwahaha! :-). */ +#else + uPort = RTRandAdvU32Ex(hRand, 1024, UINT16_MAX); +#endif + rc = ShClTransferHttpServerCreateEx(pSrv, (uint32_t)uPort); + if (RT_SUCCESS(rc)) + { + if (puPort) + *puPort = uPort; + break; + } + } + + RTRandAdvDestroy(hRand); + } + + return rc; +} + +/** + * Destroys a Shared Clipboard HTTP server instance. + * + * @returns VBox status code. + * @param pSrv HTTP server instance to destroy. + */ +int ShClTransferHttpServerDestroy(PSHCLHTTPSERVER pSrv) +{ + AssertPtrReturn(pSrv, VERR_INVALID_POINTER); + + if (pSrv->hHTTPServer == NIL_RTHTTPSERVER) + return VINF_SUCCESS; + + Assert(pSrv->cTransfers == 0); /* Sanity. */ + + int rc = RTHttpServerDestroy(pSrv->hHTTPServer); + if (RT_SUCCESS(rc)) + rc = shClTransferHttpServerDestroyInternal(pSrv); + + if (RT_SUCCESS(rc)) + LogRel2(("Shared Clipboard: HTTP server stopped\n")); + else + LogRel(("Shared Clipboard: HTTP server failed to stop, rc=%Rrc\n", rc)); + + return rc; +} + +/** + * Registers a Shared Clipboard transfer to a HTTP server instance. + * + * @returns VBox status code. + * @param pSrv HTTP server instance to register transfer for. + * @param pTransfer Transfer to register. + */ +int ShClTransferHttpServerRegisterTransfer(PSHCLHTTPSERVER pSrv, PSHCLTRANSFER pTransfer) +{ + AssertPtrReturn(pSrv, VERR_INVALID_POINTER); + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + + AssertReturn(pTransfer->State.uID, VERR_INVALID_PARAMETER); /* Paranoia. */ + + shClTransferHttpServerLock(pSrv); + + PSHCLHTTPSERVERTRANSFER pSrvTx = (PSHCLHTTPSERVERTRANSFER)RTMemAllocZ(sizeof(SHCLHTTPSERVERTRANSFER)); + AssertPtrReturn(pSrvTx, VERR_NO_MEMORY); + + RTUUID Uuid; + int rc = RTUuidCreate(&Uuid); + if (RT_SUCCESS(rc)) + { + char szUuid[64]; + rc = RTUuidToStr(&Uuid, szUuid, sizeof(szUuid)); + if (RT_SUCCESS(rc)) + { + rc = RTCritSectInit(&pSrvTx->CritSect); + AssertRC(rc); + + /* Create the virtual HTTP path for the transfer. + * Every transfer has a dedicated HTTP path. */ +#ifdef DEBUG_andy + ssize_t cch = RTStrPrintf2(pSrvTx->szPathVirtual, sizeof(pSrvTx->szPathVirtual), "/d1bbda60-80b7-45dc-a41c-ac4686c1d988/10664"); +#else + ssize_t cch = RTStrPrintf2(pSrvTx->szPathVirtual, sizeof(pSrvTx->szPathVirtual), "%s/%RU16/", szUuid, pTransfer->State.uID); +#endif + AssertReturn(cch, VERR_BUFFER_OVERFLOW); + + pSrvTx->pTransfer = pTransfer; + pSrvTx->pRootList = NULL; + pSrvTx->hObj = SHCLOBJHANDLE_INVALID; + + RTListAppend(&pSrv->lstTransfers, &pSrvTx->Node); + pSrv->cTransfers++; + + LogFunc(("pTransfer=%p, idTransfer=%RU16, szPath=%s -> %RU32 transfers\n", + pSrvTx->pTransfer, pSrvTx->pTransfer->State.uID, pSrvTx->szPathVirtual, pSrv->cTransfers)); + + LogRel2(("Shared Clipboard: Registered HTTP transfer %RU16, now %RU32 HTTP transfers total\n", + pTransfer->State.uID, pSrv->cTransfers)); + } + } + + if (RT_FAILURE(rc)) + RTMemFree(pSrvTx); + + shClTransferHttpServerUnlock(pSrv); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Unregisters a formerly registered Shared Clipboard transfer. + * + * @returns VBox status code. + * @param pSrv HTTP server instance to unregister transfer from. + * @param pTransfer Transfer to unregister. + */ +int ShClTransferHttpServerUnregisterTransfer(PSHCLHTTPSERVER pSrv, PSHCLTRANSFER pTransfer) +{ + AssertPtrReturn(pSrv, VERR_INVALID_POINTER); + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + + shClTransferHttpServerLock(pSrv); + + AssertReturn(pSrv->cTransfers, VERR_WRONG_ORDER); + + int rc = VINF_SUCCESS; + + PSHCLHTTPSERVERTRANSFER pSrvTx; + RTListForEach(&pSrv->lstTransfers, pSrvTx, SHCLHTTPSERVERTRANSFER, Node) + { + AssertPtr(pSrvTx->pTransfer); + if (pSrvTx->pTransfer->State.uID == pTransfer->State.uID) + { + RTListNodeRemove(&pSrvTx->Node); + + Assert(pSrv->cTransfers); + pSrv->cTransfers--; + + LogFunc(("pTransfer=%p, idTransfer=%RU16, szPath=%s -> %RU32 transfers\n", + pSrvTx->pTransfer, pSrvTx->pTransfer->State.uID, pSrvTx->szPathVirtual, pSrv->cTransfers)); + + LogRel2(("Shared Clipboard: Unregistered HTTP transfer %RU16, now %RU32 HTTP transfers total\n", + pTransfer->State.uID, pSrv->cTransfers)); + + rc = RTCritSectDelete(&pSrvTx->CritSect); + AssertRC(rc); + + RTMemFree(pSrvTx); + pSrvTx = NULL; + + rc = VINF_SUCCESS; + break; + } + } + + shClTransferHttpServerUnlock(pSrv); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Returns whether a specific transfer ID is registered with a HTTP server instance or not. + * + * @returns \c true if the transfer ID is registered, \c false if not. + * @param pSrv HTTP server instance. + * @param idTransfer Transfer ID to check for. + */ +bool ShClTransferHttpServerHasTransfer(PSHCLHTTPSERVER pSrv, SHCLTRANSFERID idTransfer) +{ + AssertPtrReturn(pSrv, false); + + shClTransferHttpServerLock(pSrv); + + const bool fRc = shClTransferHttpServerGetTransferById(pSrv, idTransfer) != NULL; + + shClTransferHttpServerUnlock(pSrv); + + return fRc; +} + +/** + * Returns the used TCP port number of a HTTP server instance. + * + * @returns TCP port number. 0 if not specified yet. + * @param pSrv HTTP server instance to return port for. + */ +uint16_t ShClTransferHttpServerGetPort(PSHCLHTTPSERVER pSrv) +{ + AssertPtrReturn(pSrv, 0); + + shClTransferHttpServerLock(pSrv); + + const uint16_t uPort = pSrv->uPort; + + shClTransferHttpServerUnlock(pSrv); + + return uPort; +} + +/** + * Returns the number of registered HTTP server transfers of a HTTP server instance. + * + * @returns Number of registered transfers. + * @param pSrv HTTP server instance to return registered transfers for. + */ +uint32_t ShClTransferHttpServerGetTransferCount(PSHCLHTTPSERVER pSrv) +{ + AssertPtrReturn(pSrv, 0); + + shClTransferHttpServerLock(pSrv); + + const uint32_t cTransfers = pSrv->cTransfers; + + shClTransferHttpServerUnlock(pSrv); + + return cTransfers; +} + +/** + * Returns the host name (scheme) of a HTTP server instance. + * + * @param pSrv HTTP server instance to return host name (scheme) for. + * + * @returns Host name (scheme). + */ +static const char *shClTransferHttpServerGetHost(PSHCLHTTPSERVER pSrv) +{ + RT_NOREF(pSrv); + return "http://localhost"; /* Hardcoded for now. */ +} + +/** + * Returns an allocated string with a HTTP server instance's address. + * + * @returns Allocated string with a HTTP server instance's address, or NULL on OOM. + * Needs to be free'd by the caller using RTStrFree(). + * @param pSrv HTTP server instance to return address for. + */ +char *ShClTransferHttpServerGetAddressA(PSHCLHTTPSERVER pSrv) +{ + AssertPtrReturn(pSrv, NULL); + + shClTransferHttpServerLock(pSrv); + + char *pszAddress = RTStrAPrintf2("%s:%RU16", shClTransferHttpServerGetHost(pSrv), pSrv->uPort); + AssertPtr(pszAddress); + + shClTransferHttpServerUnlock(pSrv); + + return pszAddress; +} + +/** + * Returns an allocated string with the URL of a given Shared Clipboard transfer ID. + * + * @returns Allocated string with the URL of a given Shared Clipboard transfer ID, or NULL if not found. + * Needs to be free'd by the caller using RTStrFree(). + * @param pSrv HTTP server instance to return URL for. + */ +char *ShClTransferHttpServerGetUrlA(PSHCLHTTPSERVER pSrv, SHCLTRANSFERID idTransfer) +{ + AssertPtrReturn(pSrv, NULL); + AssertReturn(idTransfer != NIL_SHCLTRANSFERID, NULL); + + shClTransferHttpServerLock(pSrv); + + PSHCLHTTPSERVERTRANSFER pSrvTx = shClTransferHttpServerGetTransferById(pSrv, idTransfer); + if (!pSrvTx) + { + AssertFailed(); + shClTransferHttpServerUnlock(pSrv); + return NULL; + } + + AssertReturn(RTStrNLen(pSrvTx->szPathVirtual, RTPATH_MAX), NULL); + char *pszUrl = RTStrAPrintf2("%s:%RU16/%s", shClTransferHttpServerGetHost(pSrv), pSrv->uPort, pSrvTx->szPathVirtual); + AssertPtr(pszUrl); + + shClTransferHttpServerUnlock(pSrv); + + return pszUrl; +} + +/** + * Returns whether a given HTTP server instance is running or not. + * + * @returns \c true if running, or \c false if not. + * @param pSrv HTTP server instance to check running state for. + */ +bool ShClTransferHttpServerIsRunning(PSHCLHTTPSERVER pSrv) +{ + AssertPtrReturn(pSrv, false); + + return (pSrv->hHTTPServer != NIL_RTHTTPSERVER); /* Seems enough for now. */ +} diff --git a/src/VBox/GuestHost/SharedClipboard/clipboard-transfers.cpp b/src/VBox/GuestHost/SharedClipboard/clipboard-transfers.cpp new file mode 100644 index 00000000..f0a0453d --- /dev/null +++ b/src/VBox/GuestHost/SharedClipboard/clipboard-transfers.cpp @@ -0,0 +1,3132 @@ +/* $Id: clipboard-transfers.cpp $ */ +/** @file + * Shared Clipboard: Common Shared Clipboard transfer handling code. + */ + +/* + * Copyright (C) 2019-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#define LOG_GROUP LOG_GROUP_SHARED_CLIPBOARD +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + +static int shClTransferThreadCreate(PSHCLTRANSFER pTransfer, PFNRTTHREAD pfnThreadFunc, void *pvUser); +static int shClTransferThreadDestroy(PSHCLTRANSFER pTransfer, RTMSINTERVAL uTimeoutMs); + +static void shclTransferCtxTransferRemoveAndUnregister(PSHCLTRANSFERCTX pTransferCtx, PSHCLTRANSFER pTransfer); +static PSHCLTRANSFER shClTransferCtxGetTransferByIdInternal(PSHCLTRANSFERCTX pTransferCtx, uint32_t uId); +static PSHCLTRANSFER shClTransferCtxGetTransferByIndexInternal(PSHCLTRANSFERCTX pTransferCtx, uint32_t uIdx); +static int shClConvertFileCreateFlags(uint32_t fShClFlags, uint64_t *pfOpen); +static int shClTransferResolvePathAbs(PSHCLTRANSFER pTransfer, const char *pszPath, uint32_t fFlags, char **ppszResolved); + +/** @todo Split this file up in different modules. */ + +/** + * Allocates a new transfer root list. + * + * @returns Allocated transfer root list on success, or NULL on failure. + */ +PSHCLROOTLIST ShClTransferRootListAlloc(void) +{ + PSHCLROOTLIST pRootList = (PSHCLROOTLIST)RTMemAllocZ(sizeof(SHCLROOTLIST)); + + return pRootList; +} + +/** + * Frees a transfer root list. + * + * @param pRootList transfer root list to free. The pointer will be + * invalid after returning from this function. + */ +void ShClTransferRootListFree(PSHCLROOTLIST pRootList) +{ + if (!pRootList) + return; + + for (uint32_t i = 0; i < pRootList->Hdr.cRoots; i++) + ShClTransferListEntryInit(&pRootList->paEntries[i]); + + RTMemFree(pRootList); + pRootList = NULL; +} + +/** + * Initializes a transfer root list header. + * + * @returns VBox status code. + * @param pRootLstHdr Root list header to initialize. + */ +int ShClTransferRootListHdrInit(PSHCLROOTLISTHDR pRootLstHdr) +{ + AssertPtrReturn(pRootLstHdr, VERR_INVALID_POINTER); + + RT_BZERO(pRootLstHdr, sizeof(SHCLROOTLISTHDR)); + + return VINF_SUCCESS; +} + +/** + * Destroys a transfer root list header. + * + * @param pRootLstHdr Root list header to destroy. + */ +void ShClTransferRootListHdrDestroy(PSHCLROOTLISTHDR pRootLstHdr) +{ + if (!pRootLstHdr) + return; + + pRootLstHdr->fRoots = 0; + pRootLstHdr->cRoots = 0; +} + +/** + * Duplicates a transfer list header. + * + * @returns Duplicated transfer list header on success, or NULL on failure. + * @param pRootLstHdr Root list header to duplicate. + */ +PSHCLROOTLISTHDR ShClTransferRootListHdrDup(PSHCLROOTLISTHDR pRootLstHdr) +{ + AssertPtrReturn(pRootLstHdr, NULL); + + int rc = VINF_SUCCESS; + + PSHCLROOTLISTHDR pRootsDup = (PSHCLROOTLISTHDR)RTMemAllocZ(sizeof(SHCLROOTLISTHDR)); + if (pRootsDup) + { + *pRootsDup = *pRootLstHdr; + } + else + rc = VERR_NO_MEMORY; + + if (RT_FAILURE(rc)) + { + ShClTransferRootListHdrDestroy(pRootsDup); + pRootsDup = NULL; + } + + return pRootsDup; +} + +/** + * (Deep) Copies a clipboard root list entry structure. + * + * @returns VBox status code. + * @param pDst Where to copy the source root list entry to. + * @param pSrc Source root list entry to copy. + */ +int ShClTransferRootListEntryCopy(PSHCLROOTLISTENTRY pDst, PSHCLROOTLISTENTRY pSrc) +{ + return ShClTransferListEntryCopy(pDst, pSrc); +} + +/** + * Initializes a clipboard root list entry structure. + * + * @param pRootListEntry Clipboard root list entry structure to destroy. + */ +int ShClTransferRootListEntryInit(PSHCLROOTLISTENTRY pRootListEntry) +{ + return ShClTransferListEntryInit(pRootListEntry); +} + +/** + * Destroys a clipboard root list entry structure. + * + * @param pRootListEntry Clipboard root list entry structure to destroy. + */ +void ShClTransferRootListEntryDestroy(PSHCLROOTLISTENTRY pRootListEntry) +{ + return ShClTransferListEntryDestroy(pRootListEntry); +} + +/** + * Duplicates (allocates) a clipboard root list entry structure. + * + * @returns Duplicated clipboard root list entry structure on success. + * @param pRootListEntry Clipboard root list entry to duplicate. + */ +PSHCLROOTLISTENTRY ShClTransferRootListEntryDup(PSHCLROOTLISTENTRY pRootListEntry) +{ + return ShClTransferListEntryDup(pRootListEntry); +} + +/** + * Initializes an list handle info structure. + * + * @returns VBox status code. + * @param pInfo List handle info structure to initialize. + */ +int ShClTransferListHandleInfoInit(PSHCLLISTHANDLEINFO pInfo) +{ + AssertPtrReturn(pInfo, VERR_INVALID_POINTER); + + pInfo->hList = SHCLLISTHANDLE_INVALID; + pInfo->enmType = SHCLOBJTYPE_INVALID; + + pInfo->pszPathLocalAbs = NULL; + + RT_ZERO(pInfo->u); + + return VINF_SUCCESS; +} + +/** + * Destroys a list handle info structure. + * + * @param pInfo List handle info structure to destroy. + */ +void ShClTransferListHandleInfoDestroy(PSHCLLISTHANDLEINFO pInfo) +{ + if (!pInfo) + return; + + if (pInfo->pszPathLocalAbs) + { + RTStrFree(pInfo->pszPathLocalAbs); + pInfo->pszPathLocalAbs = NULL; + } +} + +/** + * Allocates a transfer list header structure. + * + * @returns VBox status code. + * @param ppListHdr Where to store the allocated transfer list header structure on success. + */ +int ShClTransferListHdrAlloc(PSHCLLISTHDR *ppListHdr) +{ + int rc; + + PSHCLLISTHDR pListHdr = (PSHCLLISTHDR)RTMemAllocZ(sizeof(SHCLLISTHDR)); + if (pListHdr) + { + *ppListHdr = pListHdr; + rc = VINF_SUCCESS; + } + else + rc = VERR_NO_MEMORY; + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Frees a transfer list header structure. + * + * @param pListEntry Transfer list header structure to free. + */ +void ShClTransferListHdrFree(PSHCLLISTHDR pListHdr) +{ + if (!pListHdr) + return; + + LogFlowFuncEnter(); + + ShClTransferListHdrDestroy(pListHdr); + + RTMemFree(pListHdr); + pListHdr = NULL; +} + +/** + * Duplicates (allocates) a transfer list header structure. + * + * @returns Duplicated transfer list header structure on success. + * @param pListHdr Transfer list header to duplicate. + */ +PSHCLLISTHDR ShClTransferListHdrDup(PSHCLLISTHDR pListHdr) +{ + AssertPtrReturn(pListHdr, NULL); + + PSHCLLISTHDR pListHdrDup = (PSHCLLISTHDR)RTMemAlloc(sizeof(SHCLLISTHDR)); + if (pListHdrDup) + { + *pListHdrDup = *pListHdr; + } + + return pListHdrDup; +} + +/** + * Initializes a transfer list header structure. + * + * @returns VBox status code. + * @param pListHdr Transfer list header struct to initialize. + */ +int ShClTransferListHdrInit(PSHCLLISTHDR pListHdr) +{ + AssertPtrReturn(pListHdr, VERR_INVALID_POINTER); + + LogFlowFuncEnter(); + + ShClTransferListHdrReset(pListHdr); + + return VINF_SUCCESS; +} + +/** + * Destroys a transfer list header structure. + * + * @param pListHdr Transfer list header struct to destroy. + */ +void ShClTransferListHdrDestroy(PSHCLLISTHDR pListHdr) +{ + if (!pListHdr) + return; + + LogFlowFuncEnter(); +} + +/** + * Resets a transfer list header structure. + * + * @returns VBox status code. + * @param pListHdr Transfer list header struct to reset. + */ +void ShClTransferListHdrReset(PSHCLLISTHDR pListHdr) +{ + AssertPtrReturnVoid(pListHdr); + + LogFlowFuncEnter(); + + RT_BZERO(pListHdr, sizeof(SHCLLISTHDR)); +} + +/** + * Returns whether a given transfer list header is valid or not. + * + * @returns \c true if valid, \c false if not. + * @param pListHdr Transfer list header to validate. + */ +bool ShClTransferListHdrIsValid(PSHCLLISTHDR pListHdr) +{ + RT_NOREF(pListHdr); + return true; /** @todo Implement this. */ +} + +int ShClTransferListOpenParmsCopy(PSHCLLISTOPENPARMS pDst, PSHCLLISTOPENPARMS pSrc) +{ + AssertPtrReturn(pDst, VERR_INVALID_POINTER); + AssertPtrReturn(pSrc, VERR_INVALID_POINTER); + + int rc = VINF_SUCCESS; + + if (pSrc->pszFilter) + { + pDst->pszFilter = RTStrDup(pSrc->pszFilter); + if (!pDst->pszFilter) + rc = VERR_NO_MEMORY; + } + + if ( RT_SUCCESS(rc) + && pSrc->pszPath) + { + pDst->pszPath = RTStrDup(pSrc->pszPath); + if (!pDst->pszPath) + rc = VERR_NO_MEMORY; + } + + if (RT_SUCCESS(rc)) + { + pDst->fList = pDst->fList; + pDst->cbFilter = pSrc->cbFilter; + pDst->cbPath = pSrc->cbPath; + } + + return rc; +} + +/** + * Duplicates a transfer list open parameters structure. + * + * @returns Duplicated transfer list open parameters structure on success, or NULL on failure. + * @param pParms Transfer list open parameters structure to duplicate. + */ +PSHCLLISTOPENPARMS ShClTransferListOpenParmsDup(PSHCLLISTOPENPARMS pParms) +{ + AssertPtrReturn(pParms, NULL); + + PSHCLLISTOPENPARMS pParmsDup = (PSHCLLISTOPENPARMS)RTMemAllocZ(sizeof(SHCLLISTOPENPARMS)); + if (!pParmsDup) + return NULL; + + int rc = ShClTransferListOpenParmsCopy(pParmsDup, pParms); + if (RT_FAILURE(rc)) + { + ShClTransferListOpenParmsDestroy(pParmsDup); + + RTMemFree(pParmsDup); + pParmsDup = NULL; + } + + return pParmsDup; +} + +/** + * Initializes a transfer list open parameters structure. + * + * @returns VBox status code. + * @param pParms Transfer list open parameters structure to initialize. + */ +int ShClTransferListOpenParmsInit(PSHCLLISTOPENPARMS pParms) +{ + AssertPtrReturn(pParms, VERR_INVALID_POINTER); + + RT_BZERO(pParms, sizeof(SHCLLISTOPENPARMS)); + + pParms->cbFilter = SHCL_TRANSFER_PATH_MAX; /** @todo Make this dynamic. */ + pParms->pszFilter = RTStrAlloc(pParms->cbFilter); + + pParms->cbPath = SHCL_TRANSFER_PATH_MAX; /** @todo Make this dynamic. */ + pParms->pszPath = RTStrAlloc(pParms->cbPath); + + LogFlowFuncLeave(); + return VINF_SUCCESS; +} + +/** + * Destroys a transfer list open parameters structure. + * + * @param pParms Transfer list open parameters structure to destroy. + */ +void ShClTransferListOpenParmsDestroy(PSHCLLISTOPENPARMS pParms) +{ + if (!pParms) + return; + + if (pParms->pszFilter) + { + RTStrFree(pParms->pszFilter); + pParms->pszFilter = NULL; + } + + if (pParms->pszPath) + { + RTStrFree(pParms->pszPath); + pParms->pszPath = NULL; + } +} + +/** + * Creates (allocates) and initializes a clipboard list entry structure. + * + * @param ppDirData Where to return the created clipboard list entry structure on success. + */ +int ShClTransferListEntryAlloc(PSHCLLISTENTRY *ppListEntry) +{ + PSHCLLISTENTRY pListEntry = (PSHCLLISTENTRY)RTMemAlloc(sizeof(SHCLLISTENTRY)); + if (!pListEntry) + return VERR_NO_MEMORY; + + int rc = ShClTransferListEntryInit(pListEntry); + if (RT_SUCCESS(rc)) + *ppListEntry = pListEntry; + + return rc; +} + +/** + * Frees a clipboard list entry structure. + * + * @param pListEntry Clipboard list entry structure to free. + */ +void ShClTransferListEntryFree(PSHCLLISTENTRY pListEntry) +{ + if (!pListEntry) + return; + + ShClTransferListEntryDestroy(pListEntry); + RTMemFree(pListEntry); +} + +/** + * (Deep) Copies a clipboard list entry structure. + * + * @returns VBox status code. + * @param pListEntry Clipboard list entry to copy. + */ +int ShClTransferListEntryCopy(PSHCLLISTENTRY pDst, PSHCLLISTENTRY pSrc) +{ + AssertPtrReturn(pDst, VERR_INVALID_POINTER); + AssertPtrReturn(pSrc, VERR_INVALID_POINTER); + + int rc = VINF_SUCCESS; + + *pDst = *pSrc; + + if (pSrc->pszName) + { + pDst->pszName = RTStrDup(pSrc->pszName); + if (!pDst->pszName) + rc = VERR_NO_MEMORY; + } + + if ( RT_SUCCESS(rc) + && pSrc->pvInfo) + { + pDst->pvInfo = RTMemDup(pSrc->pvInfo, pSrc->cbInfo); + if (pDst->pvInfo) + { + pDst->cbInfo = pSrc->cbInfo; + } + else + rc = VERR_NO_MEMORY; + } + + if (RT_FAILURE(rc)) + { + if (pDst->pvInfo) + { + RTMemFree(pDst->pvInfo); + pDst->pvInfo = NULL; + pDst->cbInfo = 0; + } + } + + return rc; +} + +/** + * Duplicates (allocates) a clipboard list entry structure. + * + * @returns Duplicated clipboard list entry structure on success. + * @param pListEntry Clipboard list entry to duplicate. + */ +PSHCLLISTENTRY ShClTransferListEntryDup(PSHCLLISTENTRY pListEntry) +{ + AssertPtrReturn(pListEntry, NULL); + + int rc = VINF_SUCCESS; + + PSHCLLISTENTRY pListEntryDup = (PSHCLLISTENTRY)RTMemAllocZ(sizeof(SHCLLISTENTRY)); + if (pListEntryDup) + rc = ShClTransferListEntryCopy(pListEntryDup, pListEntry); + + if (RT_FAILURE(rc)) + { + ShClTransferListEntryDestroy(pListEntryDup); + + RTMemFree(pListEntryDup); + pListEntryDup = NULL; + } + + return pListEntryDup; +} + +/** + * Initializes a clipboard list entry structure. + * + * @returns VBox status code. + * @param pListEntry Clipboard list entry structure to initialize. + */ +int ShClTransferListEntryInit(PSHCLLISTENTRY pListEntry) +{ + AssertPtrReturn(pListEntry, VERR_INVALID_POINTER); + + RT_BZERO(pListEntry, sizeof(SHCLLISTENTRY)); + + pListEntry->pszName = RTStrAlloc(SHCLLISTENTRY_MAX_NAME); + if (!pListEntry->pszName) + return VERR_NO_MEMORY; + + pListEntry->cbName = SHCLLISTENTRY_MAX_NAME; + + pListEntry->pvInfo = (PSHCLFSOBJINFO)RTMemAlloc(sizeof(SHCLFSOBJINFO)); + if (pListEntry->pvInfo) + { + pListEntry->cbInfo = sizeof(SHCLFSOBJINFO); + pListEntry->fInfo = VBOX_SHCL_INFO_FLAG_FSOBJINFO; + + return VINF_SUCCESS; + } + + return VERR_NO_MEMORY; +} + +/** + * Destroys a clipboard list entry structure. + * + * @param pListEntry Clipboard list entry structure to destroy. + */ +void ShClTransferListEntryDestroy(PSHCLLISTENTRY pListEntry) +{ + if (!pListEntry) + return; + + if (pListEntry->pszName) + { + RTStrFree(pListEntry->pszName); + + pListEntry->pszName = NULL; + pListEntry->cbName = 0; + } + + if (pListEntry->pvInfo) + { + RTMemFree(pListEntry->pvInfo); + pListEntry->pvInfo = NULL; + pListEntry->cbInfo = 0; + } +} + +/** + * Returns whether a given clipboard list entry is valid or not. + * + * @returns \c true if valid, \c false if not. + * @param pListEntry Clipboard list entry to validate. + */ +bool ShClTransferListEntryIsValid(PSHCLLISTENTRY pListEntry) +{ + AssertPtrReturn(pListEntry, false); + + if ( !pListEntry->pszName + || !pListEntry->cbName + || strlen(pListEntry->pszName) == 0 + || strlen(pListEntry->pszName) > pListEntry->cbName /* Includes zero termination */ - 1) + { + return false; + } + + if (pListEntry->cbInfo) /* cbInfo / pvInfo is optional. */ + { + if (!pListEntry->pvInfo) + return false; + } + + return true; +} + +/** + * Initializes a transfer object context. + * + * @returns VBox status code. + * @param pObjCtx transfer object context to initialize. + */ +int ShClTransferObjCtxInit(PSHCLCLIENTTRANSFEROBJCTX pObjCtx) +{ + AssertPtrReturn(pObjCtx, VERR_INVALID_POINTER); + + LogFlowFuncEnter(); + + pObjCtx->uHandle = SHCLOBJHANDLE_INVALID; + + return VINF_SUCCESS; +} + +/** + * Destroys a transfer object context. + * + * @param pObjCtx transfer object context to destroy. + */ +void ShClTransferObjCtxDestroy(PSHCLCLIENTTRANSFEROBJCTX pObjCtx) +{ + AssertPtrReturnVoid(pObjCtx); + + LogFlowFuncEnter(); +} + +/** + * Returns if a transfer object context is valid or not. + * + * @returns \c true if valid, \c false if not. + * @param pObjCtx transfer object context to check. + */ +bool ShClTransferObjCtxIsValid(PSHCLCLIENTTRANSFEROBJCTX pObjCtx) +{ + return ( pObjCtx + && pObjCtx->uHandle != SHCLOBJHANDLE_INVALID); +} + +/** + * Initializes an object handle info structure. + * + * @returns VBox status code. + * @param pInfo Object handle info structure to initialize. + */ +int ShClTransferObjHandleInfoInit(PSHCLOBJHANDLEINFO pInfo) +{ + AssertPtrReturn(pInfo, VERR_INVALID_POINTER); + + pInfo->hObj = SHCLOBJHANDLE_INVALID; + pInfo->enmType = SHCLOBJTYPE_INVALID; + + pInfo->pszPathLocalAbs = NULL; + + RT_ZERO(pInfo->u); + + return VINF_SUCCESS; +} + +/** + * Destroys an object handle info structure. + * + * @param pInfo Object handle info structure to destroy. + */ +void ShClTransferObjHandleInfoDestroy(PSHCLOBJHANDLEINFO pInfo) +{ + if (!pInfo) + return; + + if (pInfo->pszPathLocalAbs) + { + RTStrFree(pInfo->pszPathLocalAbs); + pInfo->pszPathLocalAbs = NULL; + } +} + +/** + * Initializes a transfer object open parameters structure. + * + * @returns VBox status code. + * @param pParms transfer object open parameters structure to initialize. + */ +int ShClTransferObjOpenParmsInit(PSHCLOBJOPENCREATEPARMS pParms) +{ + AssertPtrReturn(pParms, VERR_INVALID_POINTER); + + int rc; + + RT_BZERO(pParms, sizeof(SHCLOBJOPENCREATEPARMS)); + + pParms->cbPath = RTPATH_MAX; /** @todo Make this dynamic. */ + pParms->pszPath = RTStrAlloc(pParms->cbPath); + if (pParms->pszPath) + { + rc = VINF_SUCCESS; + } + else + rc = VERR_NO_MEMORY; + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Copies a transfer object open parameters structure from source to destination. + * + * @returns VBox status code. + * @param pParmsDst Where to copy the source transfer object open parameters to. + * @param pParmsSrc Which source transfer object open parameters to copy. + */ +int ShClTransferObjOpenParmsCopy(PSHCLOBJOPENCREATEPARMS pParmsDst, PSHCLOBJOPENCREATEPARMS pParmsSrc) +{ + int rc; + + *pParmsDst = *pParmsSrc; + + if (pParmsSrc->pszPath) + { + Assert(pParmsSrc->cbPath); + pParmsDst->pszPath = RTStrDup(pParmsSrc->pszPath); + if (pParmsDst->pszPath) + { + rc = VINF_SUCCESS; + } + else + rc = VERR_NO_MEMORY; + } + else + rc = VINF_SUCCESS; + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Destroys a transfer object open parameters structure. + * + * @param pParms transfer object open parameters structure to destroy. + */ +void ShClTransferObjOpenParmsDestroy(PSHCLOBJOPENCREATEPARMS pParms) +{ + if (!pParms) + return; + + if (pParms->pszPath) + { + RTStrFree(pParms->pszPath); + pParms->pszPath = NULL; + } +} + +/** + * Returns a specific object handle info of a transfer. + * + * @returns Pointer to object handle info if found, or NULL if not found. + * @param pTransfer Clipboard transfer to get object handle info from. + * @param hObj Object handle of the object to get handle info for. + */ +DECLINLINE(PSHCLOBJHANDLEINFO) shClTransferObjGet(PSHCLTRANSFER pTransfer, SHCLOBJHANDLE hObj) +{ + PSHCLOBJHANDLEINFO pIt; + RTListForEach(&pTransfer->lstObj, pIt, SHCLOBJHANDLEINFO, Node) /** @todo Slooow ...but works for now. */ + { + if (pIt->hObj == hObj) + return pIt; + } + + return NULL; +} + +/** + * Opens a transfer object. + * + * @returns VBox status code. + * @param pTransfer Clipboard transfer to open the object for. + * @param pOpenCreateParms Open / create parameters of transfer object to open / create. + * @param phObj Where to store the handle of transfer object opened on success. + */ +int ShClTransferObjOpen(PSHCLTRANSFER pTransfer, PSHCLOBJOPENCREATEPARMS pOpenCreateParms, PSHCLOBJHANDLE phObj) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + AssertPtrReturn(pOpenCreateParms, VERR_INVALID_POINTER); + AssertPtrReturn(phObj, VERR_INVALID_POINTER); + AssertMsgReturn(pTransfer->pszPathRootAbs, ("Transfer has no root path set\n"), VERR_INVALID_PARAMETER); + AssertMsgReturn(pOpenCreateParms->pszPath, ("No path in open/create params set\n"), VERR_INVALID_PARAMETER); + + if (pTransfer->cObjHandles >= pTransfer->cMaxObjHandles) + return VERR_SHCLPB_MAX_OBJECTS_REACHED; + + LogFlowFunc(("pszPath=%s, fCreate=0x%x\n", pOpenCreateParms->pszPath, pOpenCreateParms->fCreate)); + + int rc; + if (pTransfer->State.enmSource == SHCLSOURCE_LOCAL) + { + PSHCLOBJHANDLEINFO pInfo = (PSHCLOBJHANDLEINFO)RTMemAllocZ(sizeof(SHCLOBJHANDLEINFO)); + if (pInfo) + { + rc = ShClTransferObjHandleInfoInit(pInfo); + if (RT_SUCCESS(rc)) + { + uint64_t fOpen; + rc = shClConvertFileCreateFlags(pOpenCreateParms->fCreate, &fOpen); + if (RT_SUCCESS(rc)) + { + rc = shClTransferResolvePathAbs(pTransfer, pOpenCreateParms->pszPath, 0 /* fFlags */, + &pInfo->pszPathLocalAbs); + if (RT_SUCCESS(rc)) + { + rc = RTFileOpen(&pInfo->u.Local.hFile, pInfo->pszPathLocalAbs, fOpen); + if (RT_SUCCESS(rc)) + LogRel2(("Shared Clipboard: Opened file '%s'\n", pInfo->pszPathLocalAbs)); + else + LogRel(("Shared Clipboard: Error opening file '%s': rc=%Rrc\n", pInfo->pszPathLocalAbs, rc)); + } + } + } + + if (RT_SUCCESS(rc)) + { + pInfo->hObj = pTransfer->uObjHandleNext++; + pInfo->enmType = SHCLOBJTYPE_FILE; + + RTListAppend(&pTransfer->lstObj, &pInfo->Node); + pTransfer->cObjHandles++; + + LogFlowFunc(("cObjHandles=%RU32\n", pTransfer->cObjHandles)); + + *phObj = pInfo->hObj; + } + else + { + ShClTransferObjHandleInfoDestroy(pInfo); + RTMemFree(pInfo); + } + } + else + rc = VERR_NO_MEMORY; + } + else if (pTransfer->State.enmSource == SHCLSOURCE_REMOTE) + { + if (pTransfer->ProviderIface.pfnObjOpen) + rc = pTransfer->ProviderIface.pfnObjOpen(&pTransfer->ProviderCtx, pOpenCreateParms, phObj); + else + rc = VERR_NOT_SUPPORTED; + } + else + AssertFailedStmt(rc = VERR_NOT_IMPLEMENTED); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Closes a transfer object. + * + * @returns VBox status code. + * @param pTransfer Clipboard transfer that contains the object to close. + * @param hObj Handle of transfer object to close. + */ +int ShClTransferObjClose(PSHCLTRANSFER pTransfer, SHCLOBJHANDLE hObj) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + + int rc = VINF_SUCCESS; + + if (pTransfer->State.enmSource == SHCLSOURCE_LOCAL) + { + PSHCLOBJHANDLEINFO pInfo = shClTransferObjGet(pTransfer, hObj); + if (pInfo) + { + switch (pInfo->enmType) + { + case SHCLOBJTYPE_DIRECTORY: + { + rc = RTDirClose(pInfo->u.Local.hDir); + if (RT_SUCCESS(rc)) + { + pInfo->u.Local.hDir = NIL_RTDIR; + + LogRel2(("Shared Clipboard: Closed directory '%s'\n", pInfo->pszPathLocalAbs)); + } + else + LogRel(("Shared Clipboard: Closing directory '%s' failed with %Rrc\n", pInfo->pszPathLocalAbs, rc)); + break; + } + + case SHCLOBJTYPE_FILE: + { + rc = RTFileClose(pInfo->u.Local.hFile); + if (RT_SUCCESS(rc)) + { + pInfo->u.Local.hFile = NIL_RTFILE; + + LogRel2(("Shared Clipboard: Closed file '%s'\n", pInfo->pszPathLocalAbs)); + } + else + LogRel(("Shared Clipboard: Closing file '%s' failed with %Rrc\n", pInfo->pszPathLocalAbs, rc)); + break; + } + + default: + rc = VERR_NOT_IMPLEMENTED; + break; + } + + RTListNodeRemove(&pInfo->Node); + + Assert(pTransfer->cObjHandles); + pTransfer->cObjHandles--; + + ShClTransferObjHandleInfoDestroy(pInfo); + + RTMemFree(pInfo); + pInfo = NULL; + } + else + rc = VERR_NOT_FOUND; + } + else if (pTransfer->State.enmSource == SHCLSOURCE_REMOTE) + { + if (pTransfer->ProviderIface.pfnObjClose) + { + rc = pTransfer->ProviderIface.pfnObjClose(&pTransfer->ProviderCtx, hObj); + } + else + rc = VERR_NOT_SUPPORTED; + } + else + AssertFailedStmt(rc = VERR_NOT_IMPLEMENTED); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Reads from a transfer object. + * + * @returns VBox status code. + * @param pTransfer Clipboard transfer that contains the object to read from. + * @param hObj Handle of transfer object to read from. + * @param pvBuf Buffer for where to store the read data. + * @param cbBuf Size (in bytes) of buffer. + * @param fFlags Read flags. Optional. + * @param pcbRead Where to return how much bytes were read on success. Optional. + */ +int ShClTransferObjRead(PSHCLTRANSFER pTransfer, + SHCLOBJHANDLE hObj, void *pvBuf, uint32_t cbBuf, uint32_t fFlags, uint32_t *pcbRead) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + AssertReturn (cbBuf, VERR_INVALID_PARAMETER); + /* pcbRead is optional. */ + /** @todo Validate fFlags. */ + + int rc = VINF_SUCCESS; + + if (pTransfer->State.enmSource == SHCLSOURCE_LOCAL) + { + PSHCLOBJHANDLEINFO pInfo = shClTransferObjGet(pTransfer, hObj); + if (pInfo) + { + switch (pInfo->enmType) + { + case SHCLOBJTYPE_FILE: + { + size_t cbRead; + rc = RTFileRead(pInfo->u.Local.hFile, pvBuf, cbBuf, &cbRead); + if (RT_SUCCESS(rc)) + { + if (pcbRead) + *pcbRead = (uint32_t)cbRead; + } + break; + } + + default: + rc = VERR_NOT_SUPPORTED; + break; + } + } + else + rc = VERR_NOT_FOUND; + } + else if (pTransfer->State.enmSource == SHCLSOURCE_REMOTE) + { + if (pTransfer->ProviderIface.pfnObjRead) + { + rc = pTransfer->ProviderIface.pfnObjRead(&pTransfer->ProviderCtx, hObj, pvBuf, cbBuf, fFlags, pcbRead); + } + else + rc = VERR_NOT_SUPPORTED; + } + else + AssertFailedStmt(rc = VERR_NOT_IMPLEMENTED); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Writes to a transfer object. + * + * @returns VBox status code. + * @param pTransfer Clipboard transfer that contains the object to write to. + * @param hObj Handle of transfer object to write to. + * @param pvBuf Buffer of data to write. + * @param cbBuf Size (in bytes) of buffer to write. + * @param fFlags Write flags. Optional. + * @param pcbWritten How much bytes were writtenon success. Optional. + */ +int ShClTransferObjWrite(PSHCLTRANSFER pTransfer, + SHCLOBJHANDLE hObj, void *pvBuf, uint32_t cbBuf, uint32_t fFlags, uint32_t *pcbWritten) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + AssertReturn (cbBuf, VERR_INVALID_PARAMETER); + /* pcbWritten is optional. */ + + int rc = VINF_SUCCESS; + + if (pTransfer->State.enmSource == SHCLSOURCE_LOCAL) + { + PSHCLOBJHANDLEINFO pInfo = shClTransferObjGet(pTransfer, hObj); + if (pInfo) + { + switch (pInfo->enmType) + { + case SHCLOBJTYPE_FILE: + { + rc = RTFileWrite(pInfo->u.Local.hFile, pvBuf, cbBuf, (size_t *)pcbWritten); + break; + } + + default: + rc = VERR_NOT_SUPPORTED; + break; + } + } + else + rc = VERR_NOT_FOUND; + } + else if (pTransfer->State.enmSource == SHCLSOURCE_REMOTE) + { + if (pTransfer->ProviderIface.pfnObjWrite) + { + rc = pTransfer->ProviderIface.pfnObjWrite(&pTransfer->ProviderCtx, hObj, pvBuf, cbBuf, fFlags, pcbWritten); + } + else + rc = VERR_NOT_SUPPORTED; + } + else + AssertFailedStmt(rc = VERR_NOT_IMPLEMENTED); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Duplicates a transfer object data chunk. + * + * @returns Duplicated object data chunk on success, or NULL on failure. + * @param pDataChunk transfer object data chunk to duplicate. + */ +PSHCLOBJDATACHUNK ShClTransferObjDataChunkDup(PSHCLOBJDATACHUNK pDataChunk) +{ + if (!pDataChunk) + return NULL; + + PSHCLOBJDATACHUNK pDataChunkDup = (PSHCLOBJDATACHUNK)RTMemAllocZ(sizeof(SHCLOBJDATACHUNK)); + if (!pDataChunkDup) + return NULL; + + if (pDataChunk->pvData) + { + Assert(pDataChunk->cbData); + + pDataChunkDup->uHandle = pDataChunk->uHandle; + pDataChunkDup->pvData = RTMemDup(pDataChunk->pvData, pDataChunk->cbData); + pDataChunkDup->cbData = pDataChunk->cbData; + } + + return pDataChunkDup; +} + +/** + * Destroys a transfer object data chunk. + * + * @param pDataChunk transfer object data chunk to destroy. + */ +void ShClTransferObjDataChunkDestroy(PSHCLOBJDATACHUNK pDataChunk) +{ + if (!pDataChunk) + return; + + if (pDataChunk->pvData) + { + Assert(pDataChunk->cbData); + + RTMemFree(pDataChunk->pvData); + + pDataChunk->pvData = NULL; + pDataChunk->cbData = 0; + } + + pDataChunk->uHandle = 0; +} + +/** + * Frees a transfer object data chunk. + * + * @param pDataChunk transfer object data chunk to free. The handed-in pointer will + * be invalid after calling this function. + */ +void ShClTransferObjDataChunkFree(PSHCLOBJDATACHUNK pDataChunk) +{ + if (!pDataChunk) + return; + + ShClTransferObjDataChunkDestroy(pDataChunk); + + RTMemFree(pDataChunk); + pDataChunk = NULL; +} + +/** + * Creates a clipboard transfer. + * + * @returns VBox status code. + * @param ppTransfer Where to return the created Shared Clipboard transfer struct. + * Must be destroyed by ShClTransferDestroy(). + */ +int ShClTransferCreate(PSHCLTRANSFER *ppTransfer) +{ + AssertPtrReturn(ppTransfer, VERR_INVALID_POINTER); + + LogFlowFuncEnter(); + + PSHCLTRANSFER pTransfer = (PSHCLTRANSFER)RTMemAllocZ(sizeof(SHCLTRANSFER)); + AssertPtrReturn(pTransfer, VERR_NO_MEMORY); + + pTransfer->State.uID = 0; + pTransfer->State.enmStatus = SHCLTRANSFERSTATUS_NONE; + pTransfer->State.enmDir = SHCLTRANSFERDIR_UNKNOWN; + pTransfer->State.enmSource = SHCLSOURCE_INVALID; + + pTransfer->Thread.hThread = NIL_RTTHREAD; + pTransfer->Thread.fCancelled = false; + pTransfer->Thread.fStarted = false; + pTransfer->Thread.fStop = false; + + pTransfer->pszPathRootAbs = NULL; + +#ifdef DEBUG_andy + pTransfer->uTimeoutMs = RT_MS_5SEC; +#else + pTransfer->uTimeoutMs = RT_MS_30SEC; +#endif + pTransfer->cbMaxChunkSize = _64K; /** @todo Make this configurable. */ + pTransfer->cMaxListHandles = _4K; /** @todo Ditto. */ + pTransfer->cMaxObjHandles = _4K; /** @todo Ditto. */ + + pTransfer->pvUser = NULL; + pTransfer->cbUser = 0; + + RTListInit(&pTransfer->lstList); + RTListInit(&pTransfer->lstObj); + + pTransfer->cRoots = 0; + RTListInit(&pTransfer->lstRoots); + + int rc = ShClEventSourceCreate(&pTransfer->Events, 0 /* uID */); + if (RT_SUCCESS(rc)) + { + *ppTransfer = pTransfer; + } + else + { + if (pTransfer) + { + ShClTransferDestroy(pTransfer); + RTMemFree(pTransfer); + } + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Destroys a clipboard transfer context struct. + * + * @returns VBox status code. + * @param pTransferCtx Clipboard transfer to destroy. + */ +int ShClTransferDestroy(PSHCLTRANSFER pTransfer) +{ + if (!pTransfer) + return VINF_SUCCESS; + + LogFlowFuncEnter(); + + int rc = shClTransferThreadDestroy(pTransfer, 30 * 1000 /* Timeout in ms */); + if (RT_FAILURE(rc)) + return rc; + + ShClTransferReset(pTransfer); + + ShClEventSourceDestroy(&pTransfer->Events); + + LogFlowFuncLeave(); + return VINF_SUCCESS; +} + +/** + * Initializes a Shared Clipboard transfer object. + * + * @returns VBox status code. + * @param pTransfer Transfer to initialize. + * @param enmDir Specifies the transfer direction of this transfer. + * @param enmSource Specifies the data source of the transfer. + */ +int ShClTransferInit(PSHCLTRANSFER pTransfer, SHCLTRANSFERDIR enmDir, SHCLSOURCE enmSource) +{ + pTransfer->State.enmDir = enmDir; + pTransfer->State.enmSource = enmSource; + + LogFlowFunc(("uID=%RU32, enmDir=%RU32, enmSource=%RU32\n", + pTransfer->State.uID, pTransfer->State.enmDir, pTransfer->State.enmSource)); + + pTransfer->State.enmStatus = SHCLTRANSFERSTATUS_INITIALIZED; /* Now we're ready to run. */ + + pTransfer->cListHandles = 0; + pTransfer->uListHandleNext = 1; + + pTransfer->cObjHandles = 0; + pTransfer->uObjHandleNext = 1; + + int rc = VINF_SUCCESS; + + if (pTransfer->Callbacks.pfnOnInitialize) + rc = pTransfer->Callbacks.pfnOnInitialize(&pTransfer->CallbackCtx); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Returns a specific list handle info of a transfer. + * + * @returns Pointer to list handle info if found, or NULL if not found. + * @param pTransfer Clipboard transfer to get list handle info from. + * @param hList List handle of the list to get handle info for. + */ +DECLINLINE(PSHCLLISTHANDLEINFO) shClTransferListGetByHandle(PSHCLTRANSFER pTransfer, SHCLLISTHANDLE hList) +{ + PSHCLLISTHANDLEINFO pIt; + RTListForEach(&pTransfer->lstList, pIt, SHCLLISTHANDLEINFO, Node) /** @todo Sloooow ... improve this. */ + { + if (pIt->hList == hList) + return pIt; + } + + return NULL; +} + +/** + * Creates a new list handle (local only). + * + * @returns New List handle on success, or SHCLLISTHANDLE_INVALID on error. + * @param pTransfer Clipboard transfer to create new list handle for. + */ +DECLINLINE(SHCLLISTHANDLE) shClTransferListHandleNew(PSHCLTRANSFER pTransfer) +{ + return pTransfer->uListHandleNext++; /** @todo Good enough for now. Improve this later. */ +} + +/** + * Validates whether a given path matches our set of rules or not. + * + * @returns VBox status code. + * @param pcszPath Path to validate. + * @param fMustExist Whether the path to validate also must exist. + */ +static int shClTransferValidatePath(const char *pcszPath, bool fMustExist) +{ + int rc = VINF_SUCCESS; + + if (!strlen(pcszPath)) + rc = VERR_INVALID_PARAMETER; + + if ( RT_SUCCESS(rc) + && !RTStrIsValidEncoding(pcszPath)) + { + rc = VERR_INVALID_UTF8_ENCODING; + } + + if ( RT_SUCCESS(rc) + && RTStrStr(pcszPath, "..")) + { + rc = VERR_INVALID_PARAMETER; + } + + if ( RT_SUCCESS(rc) + && fMustExist) + { + RTFSOBJINFO objInfo; + rc = RTPathQueryInfo(pcszPath, &objInfo, RTFSOBJATTRADD_NOTHING); + if (RT_SUCCESS(rc)) + { + if (RTFS_IS_DIRECTORY(objInfo.Attr.fMode)) + { + if (!RTDirExists(pcszPath)) /* Path must exist. */ + rc = VERR_PATH_NOT_FOUND; + } + else if (RTFS_IS_FILE(objInfo.Attr.fMode)) + { + if (!RTFileExists(pcszPath)) /* File must exist. */ + rc = VERR_FILE_NOT_FOUND; + } + else /* Everything else (e.g. symbolic links) are not supported. */ + { + LogRel2(("Shared Clipboard: Path '%s' contains a symbolic link or junktion, which are not supported\n", pcszPath)); + rc = VERR_NOT_SUPPORTED; + } + } + } + + if (RT_FAILURE(rc)) + LogRel2(("Shared Clipboard: Validating path '%s' failed: %Rrc\n", pcszPath, rc)); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Resolves a relative path of a specific transfer to its absolute path. + * + * @returns VBox status code. + * @param pTransfer Clipboard transfer to resolve path for. + * @param pszPath Path to resolve. + * @param fFlags Resolve flags. Currently not used and must be 0. + * @param ppszResolved Where to store the allocated resolved path. Must be free'd by the called using RTStrFree(). + */ +static int shClTransferResolvePathAbs(PSHCLTRANSFER pTransfer, const char *pszPath, uint32_t fFlags, + char **ppszResolved) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + AssertPtrReturn(pszPath, VERR_INVALID_POINTER); + AssertReturn (fFlags == 0, VERR_INVALID_PARAMETER); + + LogFlowFunc(("pszPathRootAbs=%s, pszPath=%s\n", pTransfer->pszPathRootAbs, pszPath)); + + int rc = shClTransferValidatePath(pszPath, false /* fMustExist */); + if (RT_SUCCESS(rc)) + { + char *pszPathAbs = RTPathJoinA(pTransfer->pszPathRootAbs, pszPath); + if (pszPathAbs) + { + char szResolved[RTPATH_MAX]; + size_t cbResolved = sizeof(szResolved); + rc = RTPathAbsEx(pTransfer->pszPathRootAbs, pszPathAbs, RTPATH_STR_F_STYLE_HOST, szResolved, &cbResolved); + + RTStrFree(pszPathAbs); + + if (RT_SUCCESS(rc)) + { + LogFlowFunc(("pszResolved=%s\n", szResolved)); + + rc = VERR_PATH_NOT_FOUND; /* Play safe by default. */ + + /* Make sure the resolved path is part of the set of root entries. */ + PSHCLLISTROOT pListRoot; + RTListForEach(&pTransfer->lstRoots, pListRoot, SHCLLISTROOT, Node) + { + if (RTPathStartsWith(szResolved, pListRoot->pszPathAbs)) + { + rc = VINF_SUCCESS; + break; + } + } + + if (RT_SUCCESS(rc)) + *ppszResolved = RTStrDup(szResolved); + } + } + else + rc = VERR_NO_MEMORY; + } + + if (RT_FAILURE(rc)) + LogRel(("Shared Clipboard: Resolving absolute path '%s' failed, rc=%Rrc\n", pszPath, rc)); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Opens a list. + * + * @returns VBox status code. + * @param pTransfer Clipboard transfer to handle. + * @param pOpenParms List open parameters to use for opening. + * @param phList Where to store the List handle of opened list on success. + */ +int ShClTransferListOpen(PSHCLTRANSFER pTransfer, PSHCLLISTOPENPARMS pOpenParms, + PSHCLLISTHANDLE phList) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + AssertPtrReturn(pOpenParms, VERR_INVALID_POINTER); + AssertPtrReturn(phList, VERR_INVALID_POINTER); + + int rc; + + if (pTransfer->cListHandles == pTransfer->cMaxListHandles) + return VERR_SHCLPB_MAX_LISTS_REACHED; + + if (pTransfer->State.enmSource == SHCLSOURCE_LOCAL) + { + LogFlowFunc(("pszPath=%s\n", pOpenParms->pszPath)); + + PSHCLLISTHANDLEINFO pInfo + = (PSHCLLISTHANDLEINFO)RTMemAllocZ(sizeof(SHCLLISTHANDLEINFO)); + if (pInfo) + { + rc = ShClTransferListHandleInfoInit(pInfo); + if (RT_SUCCESS(rc)) + { + rc = shClTransferResolvePathAbs(pTransfer, pOpenParms->pszPath, 0 /* fFlags */, &pInfo->pszPathLocalAbs); + if (RT_SUCCESS(rc)) + { + RTFSOBJINFO objInfo; + rc = RTPathQueryInfo(pInfo->pszPathLocalAbs, &objInfo, RTFSOBJATTRADD_NOTHING); + if (RT_SUCCESS(rc)) + { + if (RTFS_IS_DIRECTORY(objInfo.Attr.fMode)) + { + rc = RTDirOpen(&pInfo->u.Local.hDir, pInfo->pszPathLocalAbs); + if (RT_SUCCESS(rc)) + { + pInfo->enmType = SHCLOBJTYPE_DIRECTORY; + + LogRel2(("Shared Clipboard: Opening directory '%s'\n", pInfo->pszPathLocalAbs)); + } + else + LogRel(("Shared Clipboard: Opening directory '%s' failed with %Rrc\n", pInfo->pszPathLocalAbs, rc)); + + } + else if (RTFS_IS_FILE(objInfo.Attr.fMode)) + { + rc = RTFileOpen(&pInfo->u.Local.hFile, pInfo->pszPathLocalAbs, + RTFILE_O_OPEN | RTFILE_O_READ | RTFILE_O_DENY_WRITE); + if (RT_SUCCESS(rc)) + { + pInfo->enmType = SHCLOBJTYPE_FILE; + + LogRel2(("Shared Clipboard: Opening file '%s'\n", pInfo->pszPathLocalAbs)); + } + else + LogRel(("Shared Clipboard: Opening file '%s' failed with %Rrc\n", pInfo->pszPathLocalAbs, rc)); + } + else + rc = VERR_NOT_SUPPORTED; + + if (RT_SUCCESS(rc)) + { + pInfo->hList = shClTransferListHandleNew(pTransfer); + + RTListAppend(&pTransfer->lstList, &pInfo->Node); + pTransfer->cListHandles++; + + if (phList) + *phList = pInfo->hList; + + LogFlowFunc(("pszPathLocalAbs=%s, hList=%RU64, cListHandles=%RU32\n", + pInfo->pszPathLocalAbs, pInfo->hList, pTransfer->cListHandles)); + } + else + { + if (RTFS_IS_DIRECTORY(objInfo.Attr.fMode)) + { + if (RTDirIsValid(pInfo->u.Local.hDir)) + RTDirClose(pInfo->u.Local.hDir); + } + else if (RTFS_IS_FILE(objInfo.Attr.fMode)) + { + if (RTFileIsValid(pInfo->u.Local.hFile)) + RTFileClose(pInfo->u.Local.hFile); + } + } + } + } + } + + if (RT_FAILURE(rc)) + { + ShClTransferListHandleInfoDestroy(pInfo); + + RTMemFree(pInfo); + pInfo = NULL; + } + } + else + rc = VERR_NO_MEMORY; + } + else if (pTransfer->State.enmSource == SHCLSOURCE_REMOTE) + { + if (pTransfer->ProviderIface.pfnListOpen) + { + rc = pTransfer->ProviderIface.pfnListOpen(&pTransfer->ProviderCtx, pOpenParms, phList); + } + else + rc = VERR_NOT_SUPPORTED; + } + else + AssertFailedStmt(rc = VERR_NOT_IMPLEMENTED); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Closes a list. + * + * @returns VBox status code. + * @param pTransfer Clipboard transfer to handle. + * @param hList Handle of list to close. + */ +int ShClTransferListClose(PSHCLTRANSFER pTransfer, SHCLLISTHANDLE hList) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + + if (hList == SHCLLISTHANDLE_INVALID) + return VINF_SUCCESS; + + int rc = VINF_SUCCESS; + + if (pTransfer->State.enmSource == SHCLSOURCE_LOCAL) + { + PSHCLLISTHANDLEINFO pInfo = shClTransferListGetByHandle(pTransfer, hList); + if (pInfo) + { + switch (pInfo->enmType) + { + case SHCLOBJTYPE_DIRECTORY: + { + if (RTDirIsValid(pInfo->u.Local.hDir)) + { + RTDirClose(pInfo->u.Local.hDir); + pInfo->u.Local.hDir = NIL_RTDIR; + } + break; + } + + default: + rc = VERR_NOT_SUPPORTED; + break; + } + + RTListNodeRemove(&pInfo->Node); + + Assert(pTransfer->cListHandles); + pTransfer->cListHandles--; + + RTMemFree(pInfo); + } + else + rc = VERR_NOT_FOUND; + } + else if (pTransfer->State.enmSource == SHCLSOURCE_REMOTE) + { + if (pTransfer->ProviderIface.pfnListClose) + { + rc = pTransfer->ProviderIface.pfnListClose(&pTransfer->ProviderCtx, hList); + } + else + rc = VERR_NOT_SUPPORTED; + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Adds a file to a list heaer. + * + * @returns VBox status code. + * @param pHdr List header to add file to. + * @param pszPath Path of file to add. + */ +static int shclTransferListHdrAddFile(PSHCLLISTHDR pHdr, const char *pszPath) +{ + uint64_t cbSize = 0; + int rc = RTFileQuerySizeByPath(pszPath, &cbSize); + if (RT_SUCCESS(rc)) + { + pHdr->cbTotalSize += cbSize; + pHdr->cTotalObjects++; + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Builds a list header, internal version. + * + * @returns VBox status code. + * @param pHdr Where to store the build list header. + * @param pcszSrcPath Source path of list. + * @param pcszDstPath Destination path of list. + * @param pcszDstBase Destination base path. + * @param cchDstBase Number of charaters of destination base path. + */ +static int shclTransferListHdrFromDir(PSHCLLISTHDR pHdr, const char *pcszPathAbs) +{ + AssertPtrReturn(pcszPathAbs, VERR_INVALID_POINTER); + + LogFlowFunc(("pcszPathAbs=%s\n", pcszPathAbs)); + + RTFSOBJINFO objInfo; + int rc = RTPathQueryInfo(pcszPathAbs, &objInfo, RTFSOBJATTRADD_NOTHING); + if (RT_SUCCESS(rc)) + { + if (RTFS_IS_DIRECTORY(objInfo.Attr.fMode)) + { + RTDIR hDir; + rc = RTDirOpen(&hDir, pcszPathAbs); + if (RT_SUCCESS(rc)) + { + size_t cbDirEntry = 0; + PRTDIRENTRYEX pDirEntry = NULL; + do + { + /* Retrieve the next directory entry. */ + rc = RTDirReadExA(hDir, &pDirEntry, &cbDirEntry, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK); + if (RT_FAILURE(rc)) + { + if (rc == VERR_NO_MORE_FILES) + rc = VINF_SUCCESS; + break; + } + + switch (pDirEntry->Info.Attr.fMode & RTFS_TYPE_MASK) + { + case RTFS_TYPE_DIRECTORY: + { + /* Skip "." and ".." entries. */ + if (RTDirEntryExIsStdDotLink(pDirEntry)) + break; + + pHdr->cTotalObjects++; + break; + } + case RTFS_TYPE_FILE: + { + char *pszSrc = RTPathJoinA(pcszPathAbs, pDirEntry->szName); + if (pszSrc) + { + rc = shclTransferListHdrAddFile(pHdr, pszSrc); + RTStrFree(pszSrc); + } + else + rc = VERR_NO_MEMORY; + break; + } + case RTFS_TYPE_SYMLINK: + { + /** @todo Not implemented yet. */ + } + + default: + break; + } + + } while (RT_SUCCESS(rc)); + + RTDirReadExAFree(&pDirEntry, &cbDirEntry); + RTDirClose(hDir); + } + } + else if (RTFS_IS_FILE(objInfo.Attr.fMode)) + { + rc = shclTransferListHdrAddFile(pHdr, pcszPathAbs); + } + else if (RTFS_IS_SYMLINK(objInfo.Attr.fMode)) + { + /** @todo Not implemented yet. */ + } + else + rc = VERR_NOT_SUPPORTED; + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Retrieves the header of a Shared Clipboard list. + * + * @returns VBox status code. + * @param pTransfer Clipboard transfer to handle. + * @param hList Handle of list to get header for. + * @param pHdr Where to store the returned list header information. + */ +int ShClTransferListGetHeader(PSHCLTRANSFER pTransfer, SHCLLISTHANDLE hList, + PSHCLLISTHDR pHdr) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + AssertPtrReturn(pHdr, VERR_INVALID_POINTER); + + int rc; + + LogFlowFunc(("hList=%RU64\n", hList)); + + if (pTransfer->State.enmSource == SHCLSOURCE_LOCAL) + { + PSHCLLISTHANDLEINFO pInfo = shClTransferListGetByHandle(pTransfer, hList); + if (pInfo) + { + rc = ShClTransferListHdrInit(pHdr); + if (RT_SUCCESS(rc)) + { + switch (pInfo->enmType) + { + case SHCLOBJTYPE_DIRECTORY: + { + LogFlowFunc(("DirAbs: %s\n", pInfo->pszPathLocalAbs)); + + rc = shclTransferListHdrFromDir(pHdr, pInfo->pszPathLocalAbs); + break; + } + + case SHCLOBJTYPE_FILE: + { + LogFlowFunc(("FileAbs: %s\n", pInfo->pszPathLocalAbs)); + + pHdr->cTotalObjects = 1; + + RTFSOBJINFO objInfo; + rc = RTFileQueryInfo(pInfo->u.Local.hFile, &objInfo, RTFSOBJATTRADD_NOTHING); + if (RT_SUCCESS(rc)) + { + pHdr->cbTotalSize = objInfo.cbObject; + } + break; + } + + default: + rc = VERR_NOT_SUPPORTED; + break; + } + } + + LogFlowFunc(("cTotalObj=%RU64, cbTotalSize=%RU64\n", pHdr->cTotalObjects, pHdr->cbTotalSize)); + } + else + rc = VERR_NOT_FOUND; + } + else if (pTransfer->State.enmSource == SHCLSOURCE_REMOTE) + { + if (pTransfer->ProviderIface.pfnListHdrRead) + { + rc = pTransfer->ProviderIface.pfnListHdrRead(&pTransfer->ProviderCtx, hList, pHdr); + } + else + rc = VERR_NOT_SUPPORTED; + } + else + AssertFailedStmt(rc = VERR_NOT_IMPLEMENTED); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Returns the current transfer object for a Shared Clipboard transfer list. + * + * Currently not implemented and wil return NULL. + * + * @returns Pointer to transfer object, or NULL if not found / invalid. + * @param pTransfer Clipboard transfer to return transfer object for. + * @param hList Handle of Shared Clipboard transfer list to get object for. + * @param uIdx Index of object to get. + */ +PSHCLTRANSFEROBJ ShClTransferListGetObj(PSHCLTRANSFER pTransfer, + SHCLLISTHANDLE hList, uint64_t uIdx) +{ + AssertPtrReturn(pTransfer, NULL); + + RT_NOREF(hList, uIdx); + + LogFlowFunc(("hList=%RU64\n", hList)); + + return NULL; +} + +/** + * Reads a single Shared Clipboard list entry. + * + * @returns VBox status code or VERR_NO_MORE_FILES if the end of the list has been reached. + * @param pTransfer Clipboard transfer to handle. + * @param hList List handle of list to read from. + * @param pEntry Where to store the read information. + */ +int ShClTransferListRead(PSHCLTRANSFER pTransfer, SHCLLISTHANDLE hList, + PSHCLLISTENTRY pEntry) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + AssertPtrReturn(pEntry, VERR_INVALID_POINTER); + + int rc = VINF_SUCCESS; + + LogFlowFunc(("hList=%RU64\n", hList)); + + if (pTransfer->State.enmSource == SHCLSOURCE_LOCAL) + { + PSHCLLISTHANDLEINFO pInfo = shClTransferListGetByHandle(pTransfer, hList); + if (pInfo) + { + switch (pInfo->enmType) + { + case SHCLOBJTYPE_DIRECTORY: + { + LogFlowFunc(("\tDirectory: %s\n", pInfo->pszPathLocalAbs)); + + for (;;) + { + bool fSkipEntry = false; /* Whether to skip an entry in the enumeration. */ + + size_t cbDirEntry = 0; + PRTDIRENTRYEX pDirEntry = NULL; + rc = RTDirReadExA(pInfo->u.Local.hDir, &pDirEntry, &cbDirEntry, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK); + if (RT_SUCCESS(rc)) + { + switch (pDirEntry->Info.Attr.fMode & RTFS_TYPE_MASK) + { + case RTFS_TYPE_DIRECTORY: + { + /* Skip "." and ".." entries. */ + if (RTDirEntryExIsStdDotLink(pDirEntry)) + { + fSkipEntry = true; + break; + } + + LogFlowFunc(("Directory: %s\n", pDirEntry->szName)); + break; + } + + case RTFS_TYPE_FILE: + { + LogFlowFunc(("File: %s\n", pDirEntry->szName)); + break; + } + + case RTFS_TYPE_SYMLINK: + { + rc = VERR_NOT_IMPLEMENTED; /** @todo Not implemented yet. */ + break; + } + + default: + break; + } + + if ( RT_SUCCESS(rc) + && !fSkipEntry) + { + rc = RTStrCopy(pEntry->pszName, pEntry->cbName, pDirEntry->szName); + if (RT_SUCCESS(rc)) + { + pEntry->cbName = (uint32_t)strlen(pEntry->pszName) + 1; /* Include termination. */ + + AssertPtr(pEntry->pvInfo); + Assert (pEntry->cbInfo == sizeof(SHCLFSOBJINFO)); + + ShClFsObjFromIPRT(PSHCLFSOBJINFO(pEntry->pvInfo), &pDirEntry->Info); + + LogFlowFunc(("Entry pszName=%s, pvInfo=%p, cbInfo=%RU32\n", + pEntry->pszName, pEntry->pvInfo, pEntry->cbInfo)); + } + } + + RTDirReadExAFree(&pDirEntry, &cbDirEntry); + } + + if ( !fSkipEntry /* Do we have a valid entry? Bail out. */ + || RT_FAILURE(rc)) + { + break; + } + } + + break; + } + + case SHCLOBJTYPE_FILE: + { + LogFlowFunc(("\tSingle file: %s\n", pInfo->pszPathLocalAbs)); + + RTFSOBJINFO objInfo; + rc = RTFileQueryInfo(pInfo->u.Local.hFile, &objInfo, RTFSOBJATTRADD_NOTHING); + if (RT_SUCCESS(rc)) + { + pEntry->pvInfo = (PSHCLFSOBJINFO)RTMemAlloc(sizeof(SHCLFSOBJINFO)); + if (pEntry->pvInfo) + { + rc = RTStrCopy(pEntry->pszName, pEntry->cbName, pInfo->pszPathLocalAbs); + if (RT_SUCCESS(rc)) + { + ShClFsObjFromIPRT(PSHCLFSOBJINFO(pEntry->pvInfo), &objInfo); + + pEntry->cbInfo = sizeof(SHCLFSOBJINFO); + pEntry->fInfo = VBOX_SHCL_INFO_FLAG_FSOBJINFO; + } + } + else + rc = VERR_NO_MEMORY; + } + + break; + } + + default: + rc = VERR_NOT_SUPPORTED; + break; + } + } + else + rc = VERR_NOT_FOUND; + } + else if (pTransfer->State.enmSource == SHCLSOURCE_REMOTE) + { + if (pTransfer->ProviderIface.pfnListEntryRead) + rc = pTransfer->ProviderIface.pfnListEntryRead(&pTransfer->ProviderCtx, hList, pEntry); + else + rc = VERR_NOT_SUPPORTED; + } + else + AssertFailedStmt(rc = VERR_NOT_IMPLEMENTED); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +int ShClTransferListWrite(PSHCLTRANSFER pTransfer, SHCLLISTHANDLE hList, + PSHCLLISTENTRY pEntry) +{ + RT_NOREF(pTransfer, hList, pEntry); + + int rc = VINF_SUCCESS; + +#if 0 + if (pTransfer->ProviderIface.pfnListEntryWrite) + rc = pTransfer->ProviderIface.pfnListEntryWrite(&pTransfer->ProviderCtx, hList, pEntry); +#endif + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Returns whether a given list handle is valid or not. + * + * @returns \c true if list handle is valid, \c false if not. + * @param pTransfer Clipboard transfer to handle. + * @param hList List handle to check. + */ +bool ShClTransferListHandleIsValid(PSHCLTRANSFER pTransfer, SHCLLISTHANDLE hList) +{ + bool fIsValid = false; + + if (pTransfer->State.enmSource == SHCLSOURCE_LOCAL) + { + fIsValid = shClTransferListGetByHandle(pTransfer, hList) != NULL; + } + else if (pTransfer->State.enmSource == SHCLSOURCE_REMOTE) + { + AssertFailed(); /** @todo Implement. */ + } + else + AssertFailedStmt(fIsValid = false); + + return fIsValid; +} + +/** + * Copies a transfer callback table from source to destination. + * + * @param pCallbacksDst Callback destination. + * @param pCallbacksSrc Callback source. If set to NULL, the + * destination callback table will be unset. + */ +void ShClTransferCopyCallbacks(PSHCLTRANSFERCALLBACKTABLE pCallbacksDst, + PSHCLTRANSFERCALLBACKTABLE pCallbacksSrc) +{ + AssertPtrReturnVoid(pCallbacksDst); + + if (pCallbacksSrc) /* Set */ + { +#define SET_CALLBACK(a_pfnCallback) \ + if (pCallbacksSrc->a_pfnCallback) \ + pCallbacksDst->a_pfnCallback = pCallbacksSrc->a_pfnCallback + + SET_CALLBACK(pfnOnInitialize); + SET_CALLBACK(pfnOnStart); + SET_CALLBACK(pfnOnCompleted); + SET_CALLBACK(pfnOnError); + SET_CALLBACK(pfnOnRegistered); + SET_CALLBACK(pfnOnUnregistered); + +#undef SET_CALLBACK + + pCallbacksDst->pvUser = pCallbacksSrc->pvUser; + pCallbacksDst->cbUser = pCallbacksSrc->cbUser; + } + else /* Unset */ + RT_BZERO(pCallbacksDst, sizeof(SHCLTRANSFERCALLBACKTABLE)); +} + +/** + * Sets or unsets the callback table to be used for a Shared Clipboard transfer. + * + * @returns VBox status code. + * @param pTransfer Clipboard transfer to set callbacks for. + * @param pCallbacks Pointer to callback table to set. If set to NULL, + * existing callbacks for this transfer will be unset. + */ +void ShClTransferSetCallbacks(PSHCLTRANSFER pTransfer, + PSHCLTRANSFERCALLBACKTABLE pCallbacks) +{ + AssertPtrReturnVoid(pTransfer); + /* pCallbacks can be NULL. */ + + ShClTransferCopyCallbacks(&pTransfer->Callbacks, pCallbacks); +} + +/** + * Sets the transfer provider interface for a given transfer. + * + * @returns VBox status code. + * @param pTransfer Transfer to create transfer provider for. + * @param pCreationCtx Provider creation context to use for provider creation. + */ +int ShClTransferSetProviderIface(PSHCLTRANSFER pTransfer, + PSHCLTXPROVIDERCREATIONCTX pCreationCtx) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + AssertPtrReturn(pCreationCtx, VERR_INVALID_POINTER); + + LogFlowFuncEnter(); + + int rc = VINF_SUCCESS; + + pTransfer->ProviderIface = pCreationCtx->Interface; + pTransfer->ProviderCtx.pTransfer = pTransfer; + pTransfer->ProviderCtx.pvUser = pCreationCtx->pvUser; + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Clears (resets) the root list of a Shared Clipboard transfer. + * + * @param pTransfer Transfer to clear transfer root list for. + */ +static void shClTransferListRootsClear(PSHCLTRANSFER pTransfer) +{ + AssertPtrReturnVoid(pTransfer); + + if (pTransfer->pszPathRootAbs) + { + RTStrFree(pTransfer->pszPathRootAbs); + pTransfer->pszPathRootAbs = NULL; + } + + PSHCLLISTROOT pListRoot, pListRootNext; + RTListForEachSafe(&pTransfer->lstRoots, pListRoot, pListRootNext, SHCLLISTROOT, Node) + { + RTStrFree(pListRoot->pszPathAbs); + + RTListNodeRemove(&pListRoot->Node); + + RTMemFree(pListRoot); + pListRoot = NULL; + } + + pTransfer->cRoots = 0; +} + +/** + * Resets a Shared Clipboard transfer. + * + * @param pTransfer Clipboard transfer to reset. + */ +void ShClTransferReset(PSHCLTRANSFER pTransfer) +{ + AssertPtrReturnVoid(pTransfer); + + LogFlowFuncEnter(); + + shClTransferListRootsClear(pTransfer); + + PSHCLLISTHANDLEINFO pItList, pItListNext; + RTListForEachSafe(&pTransfer->lstList, pItList, pItListNext, SHCLLISTHANDLEINFO, Node) + { + ShClTransferListHandleInfoDestroy(pItList); + + RTListNodeRemove(&pItList->Node); + + RTMemFree(pItList); + } + + PSHCLOBJHANDLEINFO pItObj, pItObjNext; + RTListForEachSafe(&pTransfer->lstObj, pItObj, pItObjNext, SHCLOBJHANDLEINFO, Node) + { + ShClTransferObjHandleInfoDestroy(pItObj); + + RTListNodeRemove(&pItObj->Node); + + RTMemFree(pItObj); + } +} + +/** + * Returns the number of transfer root list entries. + * + * @returns Root list entry count. + * @param pTransfer Clipboard transfer to return root entry count for. + */ +uint32_t ShClTransferRootsCount(PSHCLTRANSFER pTransfer) +{ + AssertPtrReturn(pTransfer, 0); + + LogFlowFunc(("[Transfer %RU32] cRoots=%RU64\n", pTransfer->State.uID, pTransfer->cRoots)); + return (uint32_t)pTransfer->cRoots; +} + +/** + * Returns a specific root list entry of a transfer. + * + * @returns Pointer to root list entry if found, or NULL if not found. + * @param pTransfer Clipboard transfer to get root list entry from. + * @param uIdx Index of root list entry to return. + */ +DECLINLINE(PSHCLLISTROOT) shClTransferRootsGetInternal(PSHCLTRANSFER pTransfer, uint32_t uIdx) +{ + if (uIdx >= pTransfer->cRoots) + return NULL; + + PSHCLLISTROOT pIt = RTListGetFirst(&pTransfer->lstRoots, SHCLLISTROOT, Node); + while (uIdx--) /** @todo Slow, but works for now. */ + pIt = RTListGetNext(&pTransfer->lstRoots, pIt, SHCLLISTROOT, Node); + + return pIt; +} + +/** + * Get a specific root list entry. + * + * @returns VBox status code. + * @param pTransfer Clipboard transfer to get root list entry of. + * @param uIndex Index (zero-based) of entry to get. + * @param pEntry Where to store the returned entry on success. + */ +int ShClTransferRootsEntry(PSHCLTRANSFER pTransfer, + uint64_t uIndex, PSHCLROOTLISTENTRY pEntry) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + AssertPtrReturn(pEntry, VERR_INVALID_POINTER); + + if (uIndex >= pTransfer->cRoots) + return VERR_INVALID_PARAMETER; + + int rc; + + PSHCLLISTROOT pRoot = shClTransferRootsGetInternal(pTransfer, uIndex); + AssertPtrReturn(pRoot, VERR_INVALID_PARAMETER); + + /* Make sure that we only advertise relative source paths, not absolute ones. */ + const char *pcszSrcPath = pRoot->pszPathAbs; + + char *pszFileName = RTPathFilename(pcszSrcPath); + if (pszFileName) + { + Assert(pszFileName >= pcszSrcPath); + size_t cchDstBase = pszFileName - pcszSrcPath; + const char *pszDstPath = &pcszSrcPath[cchDstBase]; + + LogFlowFunc(("pcszSrcPath=%s, pszDstPath=%s\n", pcszSrcPath, pszDstPath)); + + rc = ShClTransferListEntryInit(pEntry); + if (RT_SUCCESS(rc)) + { + rc = RTStrCopy(pEntry->pszName, pEntry->cbName, pszDstPath); + if (RT_SUCCESS(rc)) + { + pEntry->cbInfo = sizeof(SHCLFSOBJINFO); + pEntry->pvInfo = (PSHCLFSOBJINFO)RTMemAlloc(pEntry->cbInfo); + if (pEntry->pvInfo) + { + RTFSOBJINFO fsObjInfo; + rc = RTPathQueryInfo(pcszSrcPath, &fsObjInfo, RTFSOBJATTRADD_NOTHING); + if (RT_SUCCESS(rc)) + { + ShClFsObjFromIPRT(PSHCLFSOBJINFO(pEntry->pvInfo), &fsObjInfo); + + pEntry->fInfo = VBOX_SHCL_INFO_FLAG_FSOBJINFO; + } + } + else + rc = VERR_NO_MEMORY; + } + } + } + else + rc = VERR_INVALID_POINTER; + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Returns the root entries of a Shared Clipboard transfer. + * + * @returns VBox status code. + * @param pTransfer Clipboard transfer to return root entries for. + * @param ppRootList Where to store the root list on success. + */ +int ShClTransferRootsGet(PSHCLTRANSFER pTransfer, PSHCLROOTLIST *ppRootList) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + AssertPtrReturn(ppRootList, VERR_INVALID_POINTER); + + LogFlowFuncEnter(); + + int rc = VINF_SUCCESS; + + if (pTransfer->State.enmSource == SHCLSOURCE_LOCAL) + { + PSHCLROOTLIST pRootList = ShClTransferRootListAlloc(); + if (!pRootList) + return VERR_NO_MEMORY; + + const uint64_t cRoots = (uint32_t)pTransfer->cRoots; + + LogFlowFunc(("cRoots=%RU64\n", cRoots)); + + if (cRoots) + { + PSHCLROOTLISTENTRY paRootListEntries + = (PSHCLROOTLISTENTRY)RTMemAllocZ(cRoots * sizeof(SHCLROOTLISTENTRY)); + if (paRootListEntries) + { + for (uint64_t i = 0; i < cRoots; ++i) + { + rc = ShClTransferRootsEntry(pTransfer, i, &paRootListEntries[i]); + if (RT_FAILURE(rc)) + break; + } + + if (RT_SUCCESS(rc)) + pRootList->paEntries = paRootListEntries; + } + else + rc = VERR_NO_MEMORY; + } + else + rc = VERR_NOT_FOUND; + + if (RT_SUCCESS(rc)) + { + pRootList->Hdr.cRoots = cRoots; + pRootList->Hdr.fRoots = 0; /** @todo Implement this. */ + + *ppRootList = pRootList; + } + } + else if (pTransfer->State.enmSource == SHCLSOURCE_REMOTE) + { + if (pTransfer->ProviderIface.pfnRootsGet) + rc = pTransfer->ProviderIface.pfnRootsGet(&pTransfer->ProviderCtx, ppRootList); + else + rc = VERR_NOT_SUPPORTED; + } + else + AssertFailedStmt(rc = VERR_NOT_IMPLEMENTED); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Sets transfer root list entries for a given transfer. + * + * @returns VBox status code. + * @param pTransfer Transfer to set transfer list entries for. + * @param pszRoots String list (separated by CRLF) of root entries to set. + * All entries must have the same root path. + * @param cbRoots Size (in bytes) of string list. + */ +int ShClTransferRootsSet(PSHCLTRANSFER pTransfer, const char *pszRoots, size_t cbRoots) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + AssertPtrReturn(pszRoots, VERR_INVALID_POINTER); + AssertReturn(cbRoots, VERR_INVALID_PARAMETER); + + if (!RTStrIsValidEncoding(pszRoots)) + return VERR_INVALID_UTF8_ENCODING; + + int rc = VINF_SUCCESS; + + shClTransferListRootsClear(pTransfer); + + char *pszPathRootAbs = NULL; + + RTCList lstRootEntries = RTCString(pszRoots, cbRoots - 1).split("\r\n"); + for (size_t i = 0; i < lstRootEntries.size(); ++i) + { + PSHCLLISTROOT pListRoot = (PSHCLLISTROOT)RTMemAlloc(sizeof(SHCLLISTROOT)); + AssertPtrBreakStmt(pListRoot, rc = VERR_NO_MEMORY); + + const char *pszPathCur = RTStrDup(lstRootEntries.at(i).c_str()); + + LogFlowFunc(("pszPathCur=%s\n", pszPathCur)); + + /* No root path determined yet? */ + if (!pszPathRootAbs) + { + pszPathRootAbs = RTStrDup(pszPathCur); + if (pszPathRootAbs) + { + RTPathStripFilename(pszPathRootAbs); + + LogFlowFunc(("pszPathRootAbs=%s\n", pszPathRootAbs)); + + /* We don't want to have a relative directory here. */ + if (RTPathStartsWithRoot(pszPathRootAbs)) + { + rc = shClTransferValidatePath(pszPathRootAbs, true /* Path must exist */); + } + else + rc = VERR_INVALID_PARAMETER; + } + else + rc = VERR_NO_MEMORY; + } + + if (RT_FAILURE(rc)) + break; + + pListRoot->pszPathAbs = RTStrDup(pszPathCur); + if (!pListRoot->pszPathAbs) + { + rc = VERR_NO_MEMORY; + break; + } + + RTListAppend(&pTransfer->lstRoots, &pListRoot->Node); + + pTransfer->cRoots++; + } + + /* No (valid) root directory found? Bail out early. */ + if (!pszPathRootAbs) + rc = VERR_PATH_NOT_FOUND; + + if (RT_SUCCESS(rc)) + { + /* + * Step 2: + * Go through the created list and make sure all entries have the same root path. + */ + PSHCLLISTROOT pListRoot; + RTListForEach(&pTransfer->lstRoots, pListRoot, SHCLLISTROOT, Node) + { + if (!RTStrStartsWith(pListRoot->pszPathAbs, pszPathRootAbs)) + { + rc = VERR_INVALID_PARAMETER; + break; + } + + rc = shClTransferValidatePath(pListRoot->pszPathAbs, true /* Path must exist */); + if (RT_FAILURE(rc)) + break; + } + } + + /** @todo Entry rollback on failure? */ + + if (RT_SUCCESS(rc)) + { + pTransfer->pszPathRootAbs = pszPathRootAbs; + LogFlowFunc(("pszPathRootAbs=%s, cRoots=%zu\n", pTransfer->pszPathRootAbs, pTransfer->cRoots)); + + LogRel2(("Shared Clipboard: Transfer uses root '%s'\n", pTransfer->pszPathRootAbs)); + } + else + { + LogRel(("Shared Clipboard: Unable to set roots for transfer, rc=%Rrc\n", rc)); + RTStrFree(pszPathRootAbs); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Returns the transfer's ID. + * + * @returns The transfer's ID. + * @param pTransfer Clipboard transfer to return ID for. + */ +SHCLTRANSFERID ShClTransferGetID(PSHCLTRANSFER pTransfer) +{ + AssertPtrReturn(pTransfer, 0); + + return pTransfer->State.uID; +} + +/** + * Returns the transfer's direction. + * + * @returns The transfer's direction. + * @param pTransfer Clipboard transfer to return direction for. + */ +SHCLTRANSFERDIR ShClTransferGetDir(PSHCLTRANSFER pTransfer) +{ + AssertPtrReturn(pTransfer, SHCLTRANSFERDIR_UNKNOWN); + + LogFlowFunc(("[Transfer %RU32] enmDir=%RU32\n", pTransfer->State.uID, pTransfer->State.enmDir)); + return pTransfer->State.enmDir; +} + +/** + * Returns the transfer's source. + * + * @returns The transfer's source. + * @param pTransfer Clipboard transfer to return source for. + */ +SHCLSOURCE ShClTransferGetSource(PSHCLTRANSFER pTransfer) +{ + AssertPtrReturn(pTransfer, SHCLSOURCE_INVALID); + + LogFlowFunc(("[Transfer %RU32] enmSource=%RU32\n", pTransfer->State.uID, pTransfer->State.enmSource)); + return pTransfer->State.enmSource; +} + +/** + * Returns the current transfer status. + * + * @returns Current transfer status. + * @param pTransfer Clipboard transfer to return status for. + */ +SHCLTRANSFERSTATUS ShClTransferGetStatus(PSHCLTRANSFER pTransfer) +{ + AssertPtrReturn(pTransfer, SHCLTRANSFERSTATUS_NONE); + + LogFlowFunc(("[Transfer %RU32] enmStatus=%RU32\n", pTransfer->State.uID, pTransfer->State.enmStatus)); + return pTransfer->State.enmStatus; +} + +/** + * Runs a started Shared Clipboard transfer in a dedicated thread. + * + * @returns VBox status code. + * @param pTransfer Clipboard transfer to run. + * @param pfnThreadFunc Pointer to thread function to use. + * @param pvUser Pointer to user-provided data. Optional. + */ +int ShClTransferRun(PSHCLTRANSFER pTransfer, PFNRTTHREAD pfnThreadFunc, void *pvUser) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + AssertPtrReturn(pfnThreadFunc, VERR_INVALID_POINTER); + /* pvUser is optional. */ + + AssertMsgReturn(pTransfer->State.enmStatus == SHCLTRANSFERSTATUS_STARTED, + ("Wrong status (currently is %s)\n", ShClTransferStatusToStr(pTransfer->State.enmStatus)), + VERR_WRONG_ORDER); + + int rc = shClTransferThreadCreate(pTransfer, pfnThreadFunc, pvUser); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Starts an initialized transfer. + * + * @returns VBox status code. + * @param pTransfer Clipboard transfer to start. + */ +int ShClTransferStart(PSHCLTRANSFER pTransfer) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + + LogFlowFuncEnter(); + + /* Ready to start? */ + AssertMsgReturn(pTransfer->State.enmStatus == SHCLTRANSFERSTATUS_INITIALIZED, + ("Wrong status (currently is %s)\n", ShClTransferStatusToStr(pTransfer->State.enmStatus)), + VERR_WRONG_ORDER); + + int rc; + + if (pTransfer->Callbacks.pfnOnStart) + { + rc = pTransfer->Callbacks.pfnOnStart(&pTransfer->CallbackCtx); + } + else + rc = VINF_SUCCESS; + + if (RT_SUCCESS(rc)) + { + pTransfer->State.enmStatus = SHCLTRANSFERSTATUS_STARTED; + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Creates a thread for a Shared Clipboard transfer. + * + * @returns VBox status code. + * @param pTransfer Clipboard transfer to create thread for. + * @param pfnThreadFunc Thread function to use for this transfer. + * @param pvUser Pointer to user-provided data. + */ +static int shClTransferThreadCreate(PSHCLTRANSFER pTransfer, PFNRTTHREAD pfnThreadFunc, void *pvUser) + +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + + /* Already marked for stopping? */ + AssertMsgReturn(pTransfer->Thread.fStop == false, + ("Transfer thread already marked for stopping"), VERR_WRONG_ORDER); + /* Already started? */ + AssertMsgReturn(pTransfer->Thread.fStarted == false, + ("Transfer thread already started"), VERR_WRONG_ORDER); + + /* Spawn a worker thread, so that we don't block the window thread for too long. */ + int rc = RTThreadCreate(&pTransfer->Thread.hThread, pfnThreadFunc, + pvUser, 0, RTTHREADTYPE_DEFAULT, RTTHREADFLAGS_WAITABLE, + "shclp"); + if (RT_SUCCESS(rc)) + { + int rc2 = RTThreadUserWait(pTransfer->Thread.hThread, 30 * 1000 /* Timeout in ms */); + AssertRC(rc2); + + if (pTransfer->Thread.fStarted) /* Did the thread indicate that it started correctly? */ + { + /* Nothing to do in here. */ + } + else + rc = VERR_GENERAL_FAILURE; /** @todo Find a better rc. */ + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Destroys a thread of a Shared Clipboard transfer. + * + * @returns VBox status code. + * @param pTransfer Clipboard transfer to destroy thread for. + * @param uTimeoutMs Timeout (in ms) to wait for thread creation. + */ +static int shClTransferThreadDestroy(PSHCLTRANSFER pTransfer, RTMSINTERVAL uTimeoutMs) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + + if (pTransfer->Thread.hThread == NIL_RTTHREAD) + return VINF_SUCCESS; + + LogFlowFuncEnter(); + + /* Set stop indicator. */ + pTransfer->Thread.fStop = true; + + int rcThread = VERR_WRONG_ORDER; + int rc = RTThreadWait(pTransfer->Thread.hThread, uTimeoutMs, &rcThread); + + LogFlowFunc(("Waiting for thread resulted in %Rrc (thread exited with %Rrc)\n", rc, rcThread)); + + return rc; +} + +/** + * Initializes a Shared Clipboard transfer context. + * + * @returns VBox status code. + * @param pTransferCtx Transfer context to initialize. + */ +int ShClTransferCtxInit(PSHCLTRANSFERCTX pTransferCtx) +{ + AssertPtrReturn(pTransferCtx, VERR_INVALID_POINTER); + + LogFlowFunc(("pTransferCtx=%p\n", pTransferCtx)); + + int rc = RTCritSectInit(&pTransferCtx->CritSect); + if (RT_SUCCESS(rc)) + { + RTListInit(&pTransferCtx->List); + + pTransferCtx->cTransfers = 0; + pTransferCtx->cRunning = 0; + pTransferCtx->cMaxRunning = 64; /** @todo Make this configurable? */ + + RT_ZERO(pTransferCtx->bmTransferIds); + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS_HTTP + ShClTransferHttpServerInit(&pTransferCtx->HttpServer); +#endif + ShClTransferCtxReset(pTransferCtx); + } + + return VINF_SUCCESS; +} + +/** + * Destroys a Shared Clipboard transfer context struct. + * + * @param pTransferCtx Transfer context to destroy. + */ +void ShClTransferCtxDestroy(PSHCLTRANSFERCTX pTransferCtx) +{ + if (!pTransferCtx) + return; + + LogFlowFunc(("pTransferCtx=%p\n", pTransferCtx)); + + if (RTCritSectIsInitialized(&pTransferCtx->CritSect)) + RTCritSectDelete(&pTransferCtx->CritSect); + + PSHCLTRANSFER pTransfer, pTransferNext; + RTListForEachSafe(&pTransferCtx->List, pTransfer, pTransferNext, SHCLTRANSFER, Node) + { + ShClTransferDestroy(pTransfer); + + shclTransferCtxTransferRemoveAndUnregister(pTransferCtx, pTransfer); + + RTMemFree(pTransfer); + pTransfer = NULL; + } + + pTransferCtx->cRunning = 0; + pTransferCtx->cTransfers = 0; +} + +/** + * Resets a Shared Clipboard transfer. + * + * @param pTransferCtx Transfer context to reset. + */ +void ShClTransferCtxReset(PSHCLTRANSFERCTX pTransferCtx) +{ + AssertPtrReturnVoid(pTransferCtx); + + LogFlowFuncEnter(); + + PSHCLTRANSFER pTransfer; + RTListForEach(&pTransferCtx->List, pTransfer, SHCLTRANSFER, Node) + ShClTransferReset(pTransfer); + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS_HTTP + /** @todo Anything to do here? */ +#endif +} + +/** + * Returns a specific Shared Clipboard transfer, internal version. + * + * @returns Shared Clipboard transfer, or NULL if not found. + * @param pTransferCtx Transfer context to return transfer for. + * @param uID ID of the transfer to return. + */ +static PSHCLTRANSFER shClTransferCtxGetTransferByIdInternal(PSHCLTRANSFERCTX pTransferCtx, uint32_t uID) +{ + PSHCLTRANSFER pTransfer; + RTListForEach(&pTransferCtx->List, pTransfer, SHCLTRANSFER, Node) /** @todo Slow, but works for now. */ + { + if (pTransfer->State.uID == uID) + return pTransfer; + } + + return NULL; +} + +/** + * Returns a specific Shared Clipboard transfer by index, internal version. + * + * @returns Shared Clipboard transfer, or NULL if not found. + * @param pTransferCtx Transfer context to return transfer for. + * @param uIdx Index of the transfer to return. + */ +static PSHCLTRANSFER shClTransferCtxGetTransferByIndexInternal(PSHCLTRANSFERCTX pTransferCtx, uint32_t uIdx) +{ + uint32_t idx = 0; + + PSHCLTRANSFER pTransfer; + RTListForEach(&pTransferCtx->List, pTransfer, SHCLTRANSFER, Node) /** @todo Slow, but works for now. */ + { + if (uIdx == idx) + return pTransfer; + idx++; + } + + return NULL; +} + +/** + * Returns a Shared Clipboard transfer for a specific transfer ID. + * + * @returns Shared Clipboard transfer, or NULL if not found. + * @param pTransferCtx Transfer context to return transfer for. + * @param uID ID of the transfer to return. + */ +PSHCLTRANSFER ShClTransferCtxGetTransferById(PSHCLTRANSFERCTX pTransferCtx, uint32_t uID) +{ + return shClTransferCtxGetTransferByIdInternal(pTransferCtx, uID); +} + +/** + * Returns a Shared Clipboard transfer for a specific list index. + * + * @returns Shared Clipboard transfer, or NULL if not found. + * @param pTransferCtx Transfer context to return transfer for. + * @param uIdx List index of the transfer to return. + */ +PSHCLTRANSFER ShClTransferCtxGetTransferByIndex(PSHCLTRANSFERCTX pTransferCtx, uint32_t uIdx) +{ + return shClTransferCtxGetTransferByIndexInternal(pTransferCtx, uIdx); +} + +/** + * Returns the number of running Shared Clipboard transfers. + * + * @returns Number of running transfers. + * @param pTransferCtx Transfer context to return number for. + */ +uint32_t ShClTransferCtxGetRunningTransfers(PSHCLTRANSFERCTX pTransferCtx) +{ + AssertPtrReturn(pTransferCtx, 0); + return pTransferCtx->cRunning; +} + +/** + * Returns the number of total Shared Clipboard transfers. + * + * @returns Number of total transfers. + * @param pTransferCtx Transfer context to return number for. + */ +uint32_t ShClTransferCtxGetTotalTransfers(PSHCLTRANSFERCTX pTransferCtx) +{ + AssertPtrReturn(pTransferCtx, 0); + return pTransferCtx->cTransfers; +} + +/** + * Registers a Shared Clipboard transfer with a transfer context, i.e. allocates a transfer ID. + * + * @return VBox status code. + * @retval VERR_SHCLPB_MAX_TRANSFERS_REACHED if the maximum of concurrent transfers + * is reached. + * @param pTransferCtx Transfer context to register transfer to. + * @param pTransfer Transfer to register. + * @param pidTransfer Where to return the transfer ID on success. Optional. + */ +int ShClTransferCtxTransferRegister(PSHCLTRANSFERCTX pTransferCtx, PSHCLTRANSFER pTransfer, SHCLTRANSFERID *pidTransfer) +{ + AssertPtrReturn(pTransferCtx, VERR_INVALID_POINTER); + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + /* pidTransfer is optional. */ + + /* + * Pick a random bit as starting point. If it's in use, search forward + * for a free one, wrapping around. We've reserved both the zero'th and + * max-1 IDs. + */ + SHCLTRANSFERID idTransfer = RTRandU32Ex(1, VBOX_SHCL_MAX_TRANSFERS - 2); + + if (!ASMBitTestAndSet(&pTransferCtx->bmTransferIds[0], idTransfer)) + { /* likely */ } + else if (pTransferCtx->cTransfers < VBOX_SHCL_MAX_TRANSFERS - 2 /* First and last are not used */) + { + /* Forward search. */ + int iHit = ASMBitNextClear(&pTransferCtx->bmTransferIds[0], VBOX_SHCL_MAX_TRANSFERS, idTransfer); + if (iHit < 0) + iHit = ASMBitFirstClear(&pTransferCtx->bmTransferIds[0], VBOX_SHCL_MAX_TRANSFERS); + AssertLogRelMsgReturn(iHit >= 0, ("Transfer count: %RU16\n", pTransferCtx->cTransfers), VERR_SHCLPB_MAX_TRANSFERS_REACHED); + idTransfer = iHit; + AssertLogRelMsgReturn(!ASMBitTestAndSet(&pTransferCtx->bmTransferIds[0], idTransfer), ("idObject=%#x\n", idTransfer), VERR_INTERNAL_ERROR_2); + } + else + { + LogFunc(("Maximum number of transfers reached (%RU16 transfers)\n", pTransferCtx->cTransfers)); + return VERR_SHCLPB_MAX_TRANSFERS_REACHED; + } + + Log2Func(("pTransfer=%p, idTransfer=%RU32 (%RU16 transfers)\n", pTransfer, idTransfer, pTransferCtx->cTransfers)); + + pTransfer->State.uID = idTransfer; + + RTListAppend(&pTransferCtx->List, &pTransfer->Node); + + pTransferCtx->cTransfers++; + + if (pTransfer->Callbacks.pfnOnRegistered) + pTransfer->Callbacks.pfnOnRegistered(&pTransfer->CallbackCtx, pTransferCtx); + + if (pidTransfer) + *pidTransfer = idTransfer; + + LogFlowFuncLeaveRC(VINF_SUCCESS); + return VINF_SUCCESS; +} + +/** + * Registers a Shared Clipboard transfer with a transfer context by specifying an ID for the transfer. + * + * @return VBox status code. + * @retval VERR_ALREADY_EXISTS if a transfer with the given ID already exists. + * @retval VERR_SHCLPB_MAX_TRANSFERS_REACHED if the maximum of concurrent transfers for this context has been reached. + * @param pTransferCtx Transfer context to register transfer to. + * @param pTransfer Transfer to register. + * @param idTransfer Transfer ID to use for registration. + */ +int ShClTransferCtxTransferRegisterById(PSHCLTRANSFERCTX pTransferCtx, PSHCLTRANSFER pTransfer, SHCLTRANSFERID idTransfer) +{ + LogFlowFunc(("cTransfers=%RU16, idTransfer=%RU32\n", pTransferCtx->cTransfers, idTransfer)); + + if (pTransferCtx->cTransfers < VBOX_SHCL_MAX_TRANSFERS - 2 /* First and last are not used */) + { + if (!ASMBitTestAndSet(&pTransferCtx->bmTransferIds[0], idTransfer)) + { + RTListAppend(&pTransferCtx->List, &pTransfer->Node); + + pTransfer->State.uID = idTransfer; + + if (pTransfer->Callbacks.pfnOnRegistered) + pTransfer->Callbacks.pfnOnRegistered(&pTransfer->CallbackCtx, pTransferCtx); + + pTransferCtx->cTransfers++; + return VINF_SUCCESS; + } + + return VERR_ALREADY_EXISTS; + } + + LogFunc(("Maximum number of transfers reached (%RU16 transfers)\n", pTransferCtx->cTransfers)); + return VERR_SHCLPB_MAX_TRANSFERS_REACHED; +} + +/** + * Removes and unregisters a transfer from a transfer context. + * + * @param pTransferCtx Transfer context to remove transfer from. + * @param pTransfer Transfer to remove. + */ +static void shclTransferCtxTransferRemoveAndUnregister(PSHCLTRANSFERCTX pTransferCtx, PSHCLTRANSFER pTransfer) +{ + RTListNodeRemove(&pTransfer->Node); + + Assert(pTransferCtx->cTransfers); + pTransferCtx->cTransfers--; + + Assert(pTransferCtx->cTransfers >= pTransferCtx->cRunning); + + if (pTransfer->Callbacks.pfnOnUnregistered) + pTransfer->Callbacks.pfnOnUnregistered(&pTransfer->CallbackCtx, pTransferCtx); + + LogFlowFunc(("Now %RU32 transfers left\n", pTransferCtx->cTransfers)); +} + +/** + * Unregisters a transfer from an Transfer context. + * + * @retval VINF_SUCCESS on success. + * @retval VERR_NOT_FOUND if the transfer ID was not found. + * @param pTransferCtx Transfer context to unregister transfer from. + * @param idTransfer Transfer ID to unregister. + */ +int ShClTransferCtxTransferUnregister(PSHCLTRANSFERCTX pTransferCtx, SHCLTRANSFERID idTransfer) +{ + int rc = VINF_SUCCESS; + AssertMsgStmt(ASMBitTestAndClear(&pTransferCtx->bmTransferIds, idTransfer), ("idTransfer=%#x\n", idTransfer), rc = VERR_NOT_FOUND); + + LogFlowFunc(("idTransfer=%RU32\n", idTransfer)); + + PSHCLTRANSFER pTransfer = shClTransferCtxGetTransferByIdInternal(pTransferCtx, idTransfer); + if (pTransfer) + { + shclTransferCtxTransferRemoveAndUnregister(pTransferCtx, pTransfer); + } + else + rc = VERR_NOT_FOUND; + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Cleans up all associated transfers which are not needed (anymore). + * This can be due to transfers which only have been announced but not / never being run. + * + * @param pTransferCtx Transfer context to cleanup transfers for. + */ +void ShClTransferCtxCleanup(PSHCLTRANSFERCTX pTransferCtx) +{ + AssertPtrReturnVoid(pTransferCtx); + + LogFlowFunc(("pTransferCtx=%p, cTransfers=%RU16 cRunning=%RU16\n", + pTransferCtx, pTransferCtx->cTransfers, pTransferCtx->cRunning)); + + if (pTransferCtx->cTransfers == 0) + return; + + /* Remove all transfers which are not in a running state (e.g. only announced). */ + PSHCLTRANSFER pTransfer, pTransferNext; + RTListForEachSafe(&pTransferCtx->List, pTransfer, pTransferNext, SHCLTRANSFER, Node) + { + if (ShClTransferGetStatus(pTransfer) != SHCLTRANSFERSTATUS_STARTED) + { + shclTransferCtxTransferRemoveAndUnregister(pTransferCtx, pTransfer); + + ShClTransferDestroy(pTransfer); + + RTMemFree(pTransfer); + pTransfer = NULL; + } + } +} + +/** + * Returns whether the maximum of concurrent transfers of a specific transfer contexthas been reached or not. + * + * @returns \c if maximum has been reached, \c false if not. + * @param pTransferCtx Transfer context to determine value for. + */ +bool ShClTransferCtxTransfersMaximumReached(PSHCLTRANSFERCTX pTransferCtx) +{ + AssertPtrReturn(pTransferCtx, true); + + LogFlowFunc(("cRunning=%RU32, cMaxRunning=%RU32\n", pTransferCtx->cRunning, pTransferCtx->cMaxRunning)); + + Assert(pTransferCtx->cRunning <= pTransferCtx->cMaxRunning); + return pTransferCtx->cRunning == pTransferCtx->cMaxRunning; +} + +/** + * Copies file system objinfo from IPRT to Shared Clipboard format. + * + * @param pDst The Shared Clipboard structure to convert data to. + * @param pSrc The IPRT structure to convert data from. + */ +void ShClFsObjFromIPRT(PSHCLFSOBJINFO pDst, PCRTFSOBJINFO pSrc) +{ + pDst->cbObject = pSrc->cbObject; + pDst->cbAllocated = pSrc->cbAllocated; + pDst->AccessTime = pSrc->AccessTime; + pDst->ModificationTime = pSrc->ModificationTime; + pDst->ChangeTime = pSrc->ChangeTime; + pDst->BirthTime = pSrc->BirthTime; + pDst->Attr.fMode = pSrc->Attr.fMode; + /* Clear bits which we don't pass through for security reasons. */ + pDst->Attr.fMode &= ~(RTFS_UNIX_ISUID | RTFS_UNIX_ISGID | RTFS_UNIX_ISTXT); + RT_ZERO(pDst->Attr.u); + switch (pSrc->Attr.enmAdditional) + { + default: + case RTFSOBJATTRADD_NOTHING: + pDst->Attr.enmAdditional = SHCLFSOBJATTRADD_NOTHING; + break; + + case RTFSOBJATTRADD_UNIX: + pDst->Attr.enmAdditional = SHCLFSOBJATTRADD_UNIX; + pDst->Attr.u.Unix.uid = pSrc->Attr.u.Unix.uid; + pDst->Attr.u.Unix.gid = pSrc->Attr.u.Unix.gid; + pDst->Attr.u.Unix.cHardlinks = pSrc->Attr.u.Unix.cHardlinks; + pDst->Attr.u.Unix.INodeIdDevice = pSrc->Attr.u.Unix.INodeIdDevice; + pDst->Attr.u.Unix.INodeId = pSrc->Attr.u.Unix.INodeId; + pDst->Attr.u.Unix.fFlags = pSrc->Attr.u.Unix.fFlags; + pDst->Attr.u.Unix.GenerationId = pSrc->Attr.u.Unix.GenerationId; + pDst->Attr.u.Unix.Device = pSrc->Attr.u.Unix.Device; + break; + + case RTFSOBJATTRADD_EASIZE: + pDst->Attr.enmAdditional = SHCLFSOBJATTRADD_EASIZE; + pDst->Attr.u.EASize.cb = pSrc->Attr.u.EASize.cb; + break; + } +} + +/** + * Converts Shared Clipboard create flags (see SharedClipboard-transfers.h) into IPRT create flags. + * + * @returns IPRT status code. + * @param fShClFlags Shared clipboard create flags. + * @param[out] pfOpen Where to store the RTFILE_O_XXX flags for + * RTFileOpen. + * + * @sa Initially taken from vbsfConvertFileOpenFlags(). + */ +static int shClConvertFileCreateFlags(uint32_t fShClFlags, uint64_t *pfOpen) +{ + AssertMsgReturnStmt(!(fShClFlags & ~SHCL_OBJ_CF_VALID_MASK), ("%#x4\n", fShClFlags), *pfOpen = 0, VERR_INVALID_FLAGS); + + uint64_t fOpen = 0; + + switch (fShClFlags & SHCL_OBJ_CF_ACCESS_MASK_RW) + { + case SHCL_OBJ_CF_ACCESS_NONE: + { +#ifdef RT_OS_WINDOWS + if ((fShClFlags & SHCL_OBJ_CF_ACCESS_MASK_ATTR) != SHCL_OBJ_CF_ACCESS_ATTR_NONE) + fOpen |= RTFILE_O_OPEN | RTFILE_O_ATTR_ONLY; + else +#endif + fOpen |= RTFILE_O_OPEN | RTFILE_O_READ; + LogFlowFunc(("SHCL_OBJ_CF_ACCESS_NONE\n")); + break; + } + + case SHCL_OBJ_CF_ACCESS_READ: + { + fOpen |= RTFILE_O_OPEN | RTFILE_O_READ; + LogFlowFunc(("SHCL_OBJ_CF_ACCESS_READ\n")); + break; + } + + default: + AssertFailedReturn(VERR_IPE_NOT_REACHED_DEFAULT_CASE); + } + + switch (fShClFlags & SHCL_OBJ_CF_ACCESS_MASK_ATTR) + { + case SHCL_OBJ_CF_ACCESS_ATTR_NONE: + { + fOpen |= RTFILE_O_ACCESS_ATTR_DEFAULT; + LogFlowFunc(("SHCL_OBJ_CF_ACCESS_ATTR_NONE\n")); + break; + } + + case SHCL_OBJ_CF_ACCESS_ATTR_READ: + { + fOpen |= RTFILE_O_ACCESS_ATTR_READ; + LogFlowFunc(("SHCL_OBJ_CF_ACCESS_ATTR_READ\n")); + break; + } + + default: + AssertFailedReturn(VERR_IPE_NOT_REACHED_DEFAULT_CASE); + } + + /* Sharing mask */ + switch (fShClFlags & SHCL_OBJ_CF_ACCESS_MASK_DENY) + { + case SHCL_OBJ_CF_ACCESS_DENYNONE: + fOpen |= RTFILE_O_DENY_NONE; + LogFlowFunc(("SHCL_OBJ_CF_ACCESS_DENYNONE\n")); + break; + + case SHCL_OBJ_CF_ACCESS_DENYWRITE: + fOpen |= RTFILE_O_DENY_WRITE; + LogFlowFunc(("SHCL_OBJ_CF_ACCESS_DENYWRITE\n")); + break; + + default: + AssertFailedReturn(VERR_IPE_NOT_REACHED_DEFAULT_CASE); + } + + *pfOpen = fOpen; + + LogFlowFuncLeaveRC(VINF_SUCCESS); + return VINF_SUCCESS; +} + +/** + * Translates a Shared Clipboard transfer status (SHCLTRANSFERSTATUS_XXX) into a string. + * + * @returns Transfer status string name. + * @param enmStatus The transfer status to translate. + */ +const char *ShClTransferStatusToStr(SHCLTRANSFERSTATUS enmStatus) +{ + switch (enmStatus) + { + RT_CASE_RET_STR(SHCLTRANSFERSTATUS_NONE); + RT_CASE_RET_STR(SHCLTRANSFERSTATUS_INITIALIZED); + RT_CASE_RET_STR(SHCLTRANSFERSTATUS_STARTED); + RT_CASE_RET_STR(SHCLTRANSFERSTATUS_STOPPED); + RT_CASE_RET_STR(SHCLTRANSFERSTATUS_CANCELED); + RT_CASE_RET_STR(SHCLTRANSFERSTATUS_KILLED); + RT_CASE_RET_STR(SHCLTRANSFERSTATUS_ERROR); + } + return "Unknown"; +} diff --git a/src/VBox/GuestHost/SharedClipboard/clipboard-win.cpp b/src/VBox/GuestHost/SharedClipboard/clipboard-win.cpp new file mode 100644 index 00000000..bd2a50f1 --- /dev/null +++ b/src/VBox/GuestHost/SharedClipboard/clipboard-win.cpp @@ -0,0 +1,1322 @@ +/* $Id: clipboard-win.cpp $ */ +/** @file + * Shared Clipboard: Windows-specific functions for clipboard handling. + */ + +/* + * Copyright (C) 2006-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#define LOG_GROUP LOG_GROUP_SHARED_CLIPBOARD +#include + +#include +#include +#include +#include +#include +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS +# include +# include /* For CFSTR_FILEDESCRIPTORXXX + CFSTR_FILECONTENTS. */ +# include +#endif + +#include + +#include +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS +# include +#endif +#include +#include + + +/** + * Opens the clipboard of a specific window. + * + * @returns VBox status code. + * @param hWnd Handle of window to open clipboard for. + */ +int SharedClipboardWinOpen(HWND hWnd) +{ + /* "OpenClipboard fails if another window has the clipboard open." + * So try a few times and wait up to 1 second. + */ + BOOL fOpened = FALSE; + + LogFlowFunc(("hWnd=%p\n", hWnd)); + + int i = 0; + for (;;) + { + if (OpenClipboard(hWnd)) + { + fOpened = TRUE; + break; + } + + if (i >= 10) /* sleep interval = [1..512] ms */ + break; + + RTThreadSleep(1 << i); + ++i; + } + +#ifdef LOG_ENABLED + if (i > 0) + LogFlowFunc(("%d times tried to open clipboard\n", i + 1)); +#endif + + int rc; + if (fOpened) + rc = VINF_SUCCESS; + else + { + const DWORD dwLastErr = GetLastError(); + rc = RTErrConvertFromWin32(dwLastErr); + LogRel(("Failed to open clipboard, rc=%Rrc (0x%x)\n", rc, dwLastErr)); + } + + return rc; +} + +/** + * Closes the clipboard for the current thread. + * + * @returns VBox status code. + */ +int SharedClipboardWinClose(void) +{ + int rc; + + const BOOL fRc = CloseClipboard(); + if (RT_UNLIKELY(!fRc)) + { + const DWORD dwLastErr = GetLastError(); + if (dwLastErr == ERROR_CLIPBOARD_NOT_OPEN) + { + rc = VINF_SUCCESS; /* Not important, so just report success instead. */ + } + else + { + rc = RTErrConvertFromWin32(dwLastErr); + LogFunc(("Failed with %Rrc (0x%x)\n", rc, dwLastErr)); + } + } + else + rc = VINF_SUCCESS; + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Clears the clipboard for the current thread. + * + * @returns VBox status code. + */ +int SharedClipboardWinClear(void) +{ + LogFlowFuncEnter(); + if (EmptyClipboard()) + return VINF_SUCCESS; + + const DWORD dwLastErr = GetLastError(); + AssertReturn(dwLastErr != ERROR_CLIPBOARD_NOT_OPEN, VERR_INVALID_STATE); + + int rc = RTErrConvertFromWin32(dwLastErr); + LogFunc(("Failed with %Rrc (0x%x)\n", rc, dwLastErr)); + return rc; +} + +/** + * Initializes a Shared Clipboard Windows context. + * + * @returns VBox status code. + * @param pWinCtx Shared Clipboard Windows context to initialize. + */ +int SharedClipboardWinCtxInit(PSHCLWINCTX pWinCtx) +{ + int rc = RTCritSectInit(&pWinCtx->CritSect); + if (RT_SUCCESS(rc)) + { + /* Check that new Clipboard API is available. */ + SharedClipboardWinCheckAndInitNewAPI(&pWinCtx->newAPI); + /* Do *not* check the rc, as the call might return VERR_SYMBOL_NOT_FOUND is the new API isn't available. */ + + pWinCtx->hWnd = NULL; + pWinCtx->hWndClipboardOwnerUs = NULL; + pWinCtx->hWndNextInChain = NULL; + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Destroys a Shared Clipboard Windows context. + * + * @param pWinCtx Shared Clipboard Windows context to destroy. + */ +void SharedClipboardWinCtxDestroy(PSHCLWINCTX pWinCtx) +{ + if (!pWinCtx) + return; + + LogFlowFuncEnter(); + + if (RTCritSectIsInitialized(&pWinCtx->CritSect)) + { + int rc2 = RTCritSectDelete(&pWinCtx->CritSect); + AssertRC(rc2); + } +} + +/** + * Checks and initializes function pointer which are required for using + * the new clipboard API. + * + * @returns VBox status code, or VERR_SYMBOL_NOT_FOUND if the new API is not available. + * @param pAPI Where to store the retrieved function pointers. + * Will be set to NULL if the new API is not available. + */ +int SharedClipboardWinCheckAndInitNewAPI(PSHCLWINAPINEW pAPI) +{ + RTLDRMOD hUser32 = NIL_RTLDRMOD; + int rc = RTLdrLoadSystem("User32.dll", /* fNoUnload = */ true, &hUser32); + if (RT_SUCCESS(rc)) + { + rc = RTLdrGetSymbol(hUser32, "AddClipboardFormatListener", (void **)&pAPI->pfnAddClipboardFormatListener); + if (RT_SUCCESS(rc)) + { + rc = RTLdrGetSymbol(hUser32, "RemoveClipboardFormatListener", (void **)&pAPI->pfnRemoveClipboardFormatListener); + } + + RTLdrClose(hUser32); + } + + if (RT_SUCCESS(rc)) + { + LogRel(("Shared Clipboard: New Clipboard API enabled\n")); + } + else + { + RT_BZERO(pAPI, sizeof(SHCLWINAPINEW)); + LogRel(("Shared Clipboard: New Clipboard API not available (%Rrc)\n", rc)); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Returns if the new clipboard API is available or not. + * + * @returns @c true if the new API is available, or @c false if not. + * @param pAPI Structure used for checking if the new clipboard API is available or not. + */ +bool SharedClipboardWinIsNewAPI(PSHCLWINAPINEW pAPI) +{ + if (!pAPI) + return false; + return pAPI->pfnAddClipboardFormatListener != NULL; +} + +/** + * Adds ourselves into the chain of cliboard listeners. + * + * @returns VBox status code. + * @param pCtx Windows clipboard context to use to add ourselves. + */ +int SharedClipboardWinChainAdd(PSHCLWINCTX pCtx) +{ + const PSHCLWINAPINEW pAPI = &pCtx->newAPI; + + BOOL fRc; + if (SharedClipboardWinIsNewAPI(pAPI)) + fRc = pAPI->pfnAddClipboardFormatListener(pCtx->hWnd); + else + { + SetLastError(NO_ERROR); + pCtx->hWndNextInChain = SetClipboardViewer(pCtx->hWnd); + fRc = pCtx->hWndNextInChain != NULL || GetLastError() == NO_ERROR; + } + + int rc = VINF_SUCCESS; + + if (!fRc) + { + const DWORD dwLastErr = GetLastError(); + rc = RTErrConvertFromWin32(dwLastErr); + LogFunc(("Failed with %Rrc (0x%x)\n", rc, dwLastErr)); + } + + return rc; +} + +/** + * Remove ourselves from the chain of cliboard listeners + * + * @returns VBox status code. + * @param pCtx Windows clipboard context to use to remove ourselves. + */ +int SharedClipboardWinChainRemove(PSHCLWINCTX pCtx) +{ + if (!pCtx->hWnd) + return VINF_SUCCESS; + + const PSHCLWINAPINEW pAPI = &pCtx->newAPI; + + BOOL fRc; + if (SharedClipboardWinIsNewAPI(pAPI)) + { + fRc = pAPI->pfnRemoveClipboardFormatListener(pCtx->hWnd); + } + else + { + fRc = ChangeClipboardChain(pCtx->hWnd, pCtx->hWndNextInChain); + if (fRc) + pCtx->hWndNextInChain = NULL; + } + + int rc = VINF_SUCCESS; + + if (!fRc) + { + const DWORD dwLastErr = GetLastError(); + rc = RTErrConvertFromWin32(dwLastErr); + LogFunc(("Failed with %Rrc (0x%x)\n", rc, dwLastErr)); + } + + return rc; +} + +/** + * Callback which is invoked when we have successfully pinged ourselves down the + * clipboard chain. We simply unset a boolean flag to say that we are responding. + * There is a race if a ping returns after the next one is initiated, but nothing + * very bad is likely to happen. + * + * @param hWnd Window handle to use for this callback. Not used currently. + * @param uMsg Message to handle. Not used currently. + * @param dwData Pointer to user-provided data. Contains our Windows clipboard context. + * @param lResult Additional data to pass. Not used currently. + */ +VOID CALLBACK SharedClipboardWinChainPingProc(HWND hWnd, UINT uMsg, ULONG_PTR dwData, LRESULT lResult) RT_NOTHROW_DEF +{ + RT_NOREF(hWnd); + RT_NOREF(uMsg); + RT_NOREF(lResult); + + /** @todo r=andy Why not using SetWindowLongPtr for keeping the context? */ + PSHCLWINCTX pCtx = (PSHCLWINCTX)dwData; + AssertPtrReturnVoid(pCtx); + + pCtx->oldAPI.fCBChainPingInProcess = FALSE; +} + +/** + * Passes a window message to the next window in the clipboard chain. + * + * @returns LRESULT + * @param pWinCtx Window context to use. + * @param msg Window message to pass. + * @param wParam WPARAM to pass. + * @param lParam LPARAM to pass. + */ +LRESULT SharedClipboardWinChainPassToNext(PSHCLWINCTX pWinCtx, + UINT msg, WPARAM wParam, LPARAM lParam) +{ + LogFlowFuncEnter(); + + LRESULT lresultRc = 0; + + if (pWinCtx->hWndNextInChain) + { + LogFunc(("hWndNextInChain=%p\n", pWinCtx->hWndNextInChain)); + + /* Pass the message to next window in the clipboard chain. */ + DWORD_PTR dwResult; + lresultRc = SendMessageTimeout(pWinCtx->hWndNextInChain, msg, wParam, lParam, 0, + SHCL_WIN_CBCHAIN_TIMEOUT_MS, &dwResult); + if (!lresultRc) + lresultRc = dwResult; + } + + LogFlowFunc(("lresultRc=%ld\n", lresultRc)); + return lresultRc; +} + +/** + * Converts a (registered or standard) Windows clipboard format to a VBox clipboard format. + * + * @returns Converted VBox clipboard format, or VBOX_SHCL_FMT_NONE if not found. + * @param uFormat Windows clipboard format to convert. + */ +SHCLFORMAT SharedClipboardWinClipboardFormatToVBox(UINT uFormat) +{ + /* Insert the requested clipboard format data into the clipboard. */ + SHCLFORMAT vboxFormat = VBOX_SHCL_FMT_NONE; + + switch (uFormat) + { + case CF_UNICODETEXT: + vboxFormat = VBOX_SHCL_FMT_UNICODETEXT; + break; + + case CF_DIB: + vboxFormat = VBOX_SHCL_FMT_BITMAP; + break; + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + /* CF_HDROP handles file system entries which are locally present + * on source for transferring to the target. + * + * This does *not* invoke any IDataObject / IStream implementations! */ + case CF_HDROP: + vboxFormat = VBOX_SHCL_FMT_URI_LIST; + break; +#endif + + default: + if (uFormat >= 0xC000) /** Formats registered with RegisterClipboardFormat() start at this index. */ + { + TCHAR szFormatName[256]; /** @todo r=andy Do we need Unicode support here as well? */ + int cActual = GetClipboardFormatName(uFormat, szFormatName, sizeof(szFormatName) / sizeof(TCHAR)); + if (cActual) + { + LogFlowFunc(("uFormat=%u -> szFormatName=%s\n", uFormat, szFormatName)); + + if (RTStrCmp(szFormatName, SHCL_WIN_REGFMT_HTML) == 0) + vboxFormat = VBOX_SHCL_FMT_HTML; +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + /* These types invoke our IDataObject / IStream implementations. */ + else if ( (RTStrCmp(szFormatName, CFSTR_FILEDESCRIPTORA) == 0) + || (RTStrCmp(szFormatName, CFSTR_FILECONTENTS) == 0)) + vboxFormat = VBOX_SHCL_FMT_URI_LIST; + /** @todo Do we need to handle CFSTR_FILEDESCRIPTORW here as well? */ +#endif + } + } + break; + } + + LogFlowFunc(("uFormat=%u -> vboxFormat=0x%x\n", uFormat, vboxFormat)); + return vboxFormat; +} + +/** + * Retrieves all supported clipboard formats of a specific clipboard. + * + * @returns VBox status code. + * @param pCtx Windows clipboard context to retrieve formats for. + * @param pfFormats Where to store the retrieved formats. + */ +int SharedClipboardWinGetFormats(PSHCLWINCTX pCtx, PSHCLFORMATS pfFormats) +{ + AssertPtrReturn(pCtx, VERR_INVALID_POINTER); + AssertPtrReturn(pfFormats, VERR_INVALID_POINTER); + + SHCLFORMATS fFormats = VBOX_SHCL_FMT_NONE; + + /* Query list of available formats and report to host. */ + int rc = SharedClipboardWinOpen(pCtx->hWnd); + if (RT_SUCCESS(rc)) + { + UINT uCurFormat = 0; /* Must be set to zero for EnumClipboardFormats(). */ + while ((uCurFormat = EnumClipboardFormats(uCurFormat)) != 0) + fFormats |= SharedClipboardWinClipboardFormatToVBox(uCurFormat); + + int rc2 = SharedClipboardWinClose(); + AssertRC(rc2); + LogFlowFunc(("fFormats=%#x\n", fFormats)); + } + else + LogFunc(("Failed with rc=%Rrc (fFormats=%#x)\n", rc, fFormats)); + + *pfFormats = fFormats; + return rc; +} + +/** + * Extracts a field value from CF_HTML data. + * + * @returns VBox status code. + * @param pszSrc source in CF_HTML format. + * @param pszOption Name of CF_HTML field. + * @param puValue Where to return extracted value of CF_HTML field. + */ +int SharedClipboardWinGetCFHTMLHeaderValue(const char *pszSrc, const char *pszOption, uint32_t *puValue) +{ + AssertPtrReturn(pszSrc, VERR_INVALID_POINTER); + AssertPtrReturn(pszOption, VERR_INVALID_POINTER); + + int rc = VERR_INVALID_PARAMETER; + + const char *pszOptionValue = RTStrStr(pszSrc, pszOption); + if (pszOptionValue) + { + size_t cchOption = strlen(pszOption); + Assert(cchOption); + + rc = RTStrToUInt32Ex(pszOptionValue + cchOption, NULL, 10, puValue); + } + return rc; +} + +/** + * Check that the source string contains CF_HTML struct. + * + * @returns @c true if the @a pszSource string is in CF_HTML format. + * @param pszSource Source string to check. + */ +bool SharedClipboardWinIsCFHTML(const char *pszSource) +{ + return RTStrStr(pszSource, "Version:") != NULL + && RTStrStr(pszSource, "StartHTML:") != NULL; +} + +/** + * Converts clipboard data from CF_HTML format to MIME clipboard format. + * + * Returns allocated buffer that contains html converted to text/html mime type + * + * @returns VBox status code. + * @param pszSource The input. + * @param cch The length of the input. + * @param ppszOutput Where to return the result. Free using RTMemFree. + * @param pcbOutput Where to the return length of the result (bytes/chars). + */ +int SharedClipboardWinConvertCFHTMLToMIME(const char *pszSource, const uint32_t cch, char **ppszOutput, uint32_t *pcbOutput) +{ + Assert(pszSource); + Assert(cch); + Assert(ppszOutput); + Assert(pcbOutput); + + uint32_t offStart; + int rc = SharedClipboardWinGetCFHTMLHeaderValue(pszSource, "StartFragment:", &offStart); + if (RT_SUCCESS(rc)) + { + uint32_t offEnd; + rc = SharedClipboardWinGetCFHTMLHeaderValue(pszSource, "EndFragment:", &offEnd); + if (RT_SUCCESS(rc)) + { + if ( offStart > 0 + && offEnd > 0 + && offEnd >= offStart + && offEnd <= cch) + { + uint32_t cchSubStr = offEnd - offStart; + char *pszResult = (char *)RTMemAlloc(cchSubStr + 1); + if (pszResult) + { + rc = RTStrCopyEx(pszResult, cchSubStr + 1, pszSource + offStart, cchSubStr); + if (RT_SUCCESS(rc)) + { + *ppszOutput = pszResult; + *pcbOutput = (uint32_t)(cchSubStr + 1); + rc = VINF_SUCCESS; + } + else + { + LogRelFlowFunc(("Error: Unknown CF_HTML format. Expected EndFragment. rc = %Rrc\n", rc)); + RTMemFree(pszResult); + } + } + else + { + LogRelFlowFunc(("Error: Unknown CF_HTML format. Expected EndFragment\n")); + rc = VERR_NO_MEMORY; + } + } + else + { + LogRelFlowFunc(("Error: CF_HTML out of bounds - offStart=%#x offEnd=%#x cch=%#x\n", offStart, offEnd, cch)); + rc = VERR_INVALID_PARAMETER; + } + } + else + { + LogRelFlowFunc(("Error: Unknown CF_HTML format. Expected EndFragment. rc = %Rrc\n", rc)); + rc = VERR_INVALID_PARAMETER; + } + } + else + { + LogRelFlowFunc(("Error: Unknown CF_HTML format. Expected StartFragment. rc = %Rrc\n", rc)); + rc = VERR_INVALID_PARAMETER; + } + + return rc; +} + +/** + * Converts source UTF-8 MIME HTML clipboard data to UTF-8 CF_HTML format. + * + * This is just encapsulation work, slapping a header on the data. + * + * It allocates [..] + * + * Calculations: + * Header length = format Length + (2*(10 - 5('%010d'))('digits')) - 2('%s') = format length + 8 + * EndHtml = Header length + fragment length + * StartHtml = 105(constant) + * StartFragment = 141(constant) may vary if the header html content will be extended + * EndFragment = Header length + fragment length - 38(ending length) + * + * For more format details, check out: + * https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767917(v=vs.85) + * + * @returns VBox status code. + * @param pszSource Source buffer that contains utf-16 string in mime html format + * @param cb Size of source buffer in bytes + * @param ppszOutput Where to return the allocated output buffer to put converted UTF-8 + * CF_HTML clipboard data. This function allocates memory for this. + * @param pcbOutput Where to return the size of allocated result buffer in bytes/chars, including zero terminator + * + * @note output buffer should be free using RTMemFree() + * @note Everything inside of fragment can be UTF8. Windows allows it. Everything in header should be Latin1. + */ +int SharedClipboardWinConvertMIMEToCFHTML(const char *pszSource, size_t cb, char **ppszOutput, uint32_t *pcbOutput) +{ + Assert(ppszOutput); + Assert(pcbOutput); + Assert(pszSource); + Assert(cb); + + /* + * Check that input UTF-8 and properly zero terminated. + * Note! The zero termination may come earlier than 'cb' - 1, that's fine. + */ + int rc = RTStrValidateEncodingEx(pszSource, cb, RTSTR_VALIDATE_ENCODING_ZERO_TERMINATED); + if (RT_SUCCESS(rc)) + { /* likely */ } + else + { + LogRelFlowFunc(("Error: invalid source fragment. rc = %Rrc\n", rc)); + return rc; + } + size_t const cchFragment = strlen(pszSource); /* Unfortunately the validator doesn't return the length. */ + + /* + * @StartHtml - Absolute offset of + * @EndHtml - Size of the whole resulting text (excluding ending zero char) + * @StartFragment - Absolute position after + * @EndFragment - Absolute position of + * + * Note! The offset are zero padded to max width so we don't have any variations due to those. + * Note! All values includes CRLFs inserted into text. + * + * Calculations: + * Header length = Format sample length - 2 ('%s') + * EndHtml = Header length + fragment length + * StartHtml = 101(constant) + * StartFragment = 137(constant) + * EndFragment = Header length + fragment length - 38 (ending length) + */ + static const char s_szFormatSample[] = + /* 0: */ "Version:1.0\r\n" + /* 13: */ "StartHTML:000000101\r\n" + /* 34: */ "EndHTML:%0000009u\r\n" // END HTML = Header length + fragment length + /* 53: */ "StartFragment:000000137\r\n" + /* 78: */ "EndFragment:%0000009u\r\n" + /* 101: */ "\r\n" + /* 109: */ "\r\n" + /* 117: */ "" + /* 137: */ "%s" + /* 137+2: */ "\r\n" + /* 157+2: */ "\r\n" + /* 166+2: */ "\r\n" + /* 175+2: */ ; + AssertCompile(sizeof(s_szFormatSample) == 175 + 2 + 1); + + /* Calculate parameters of the CF_HTML header */ + size_t const cchHeader = sizeof(s_szFormatSample) - 2 /*%s*/ - 1 /*'\0'*/; + size_t const offEndHtml = cchHeader + cchFragment; + size_t const offEndFragment = cchHeader + cchFragment - 38; /* 175-137 = 38 */ + char *pszResult = (char *)RTMemAlloc(offEndHtml + 1); + AssertLogRelReturn(pszResult, VERR_NO_MEMORY); + + /* Format resulting CF_HTML string: */ + size_t cchFormatted = RTStrPrintf(pszResult, offEndHtml + 1, s_szFormatSample, offEndHtml, offEndFragment, pszSource); + Assert(offEndHtml == cchFormatted); + +#ifdef VBOX_STRICT + /* + * Check the calculations. + */ + + /* check 'StartFragment:' value */ + static const char s_szStartFragment[] = ""; + const char *pszRealStartFragment = RTStrStr(pszResult, s_szStartFragment); + Assert(&pszRealStartFragment[sizeof(s_szStartFragment) - 1] - pszResult == 137); + + /* check 'EndFragment:' value */ + static const char s_szEndFragment[] = ""; + const char *pszRealEndFragment = RTStrStr(pszResult, s_szEndFragment); + Assert((size_t)(pszRealEndFragment - pszResult) == offEndFragment); +#endif + + *ppszOutput = pszResult; + *pcbOutput = (uint32_t)cchFormatted + 1; + Assert(*pcbOutput == cchFormatted + 1); + + return VINF_SUCCESS; +} + +/** + * Handles the WM_CHANGECBCHAIN code. + * + * @returns LRESULT + * @param pWinCtx Windows context to use. + * @param hWnd Window handle to use. + * @param msg Message ID to pass on. + * @param wParam wParam to pass on + * @param lParam lParam to pass on. + */ +LRESULT SharedClipboardWinHandleWMChangeCBChain(PSHCLWINCTX pWinCtx, + HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + LRESULT lresultRc = 0; + + LogFlowFuncEnter(); + + if (SharedClipboardWinIsNewAPI(&pWinCtx->newAPI)) + { + lresultRc = DefWindowProc(hWnd, msg, wParam, lParam); + } + else /* Old API */ + { + HWND hwndRemoved = (HWND)wParam; + HWND hwndNext = (HWND)lParam; + + if (hwndRemoved == pWinCtx->hWndNextInChain) + { + /* The window that was next to our in the chain is being removed. + * Relink to the new next window. + */ + pWinCtx->hWndNextInChain = hwndNext; + } + else + { + if (pWinCtx->hWndNextInChain) + { + /* Pass the message further. */ + DWORD_PTR dwResult; + lresultRc = SendMessageTimeout(pWinCtx->hWndNextInChain, WM_CHANGECBCHAIN, wParam, lParam, 0, + SHCL_WIN_CBCHAIN_TIMEOUT_MS, + &dwResult); + if (!lresultRc) + lresultRc = (LRESULT)dwResult; + } + } + } + + LogFlowFunc(("lresultRc=%ld\n", lresultRc)); + return lresultRc; +} + +/** + * Handles the WM_DESTROY code. + * + * @returns VBox status code. + * @param pWinCtx Windows context to use. + */ +int SharedClipboardWinHandleWMDestroy(PSHCLWINCTX pWinCtx) +{ + LogFlowFuncEnter(); + + int rc = VINF_SUCCESS; + + /* MS recommends to remove from Clipboard chain in this callback. */ + SharedClipboardWinChainRemove(pWinCtx); + + if (pWinCtx->oldAPI.timerRefresh) + { + Assert(pWinCtx->hWnd); + KillTimer(pWinCtx->hWnd, 0); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Handles the WM_RENDERALLFORMATS message. + * + * @returns VBox status code. + * @param pWinCtx Windows context to use. + * @param hWnd Window handle to use. + */ +int SharedClipboardWinHandleWMRenderAllFormats(PSHCLWINCTX pWinCtx, HWND hWnd) +{ + RT_NOREF(pWinCtx); + + LogFlowFuncEnter(); + + /* Do nothing. The clipboard formats will be unavailable now, because the + * windows is to be destroyed and therefore the guest side becomes inactive. + */ + int rc = SharedClipboardWinOpen(hWnd); + if (RT_SUCCESS(rc)) + { + SharedClipboardWinClear(); + SharedClipboardWinClose(); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Handles the WM_TIMER code, which is needed if we're running with the so-called "old" Windows clipboard API. + * Does nothing if we're running with the "new" Windows API. + * + * @returns VBox status code. + * @param pWinCtx Windows context to use. + */ +int SharedClipboardWinHandleWMTimer(PSHCLWINCTX pWinCtx) +{ + int rc = VINF_SUCCESS; + + if (!SharedClipboardWinIsNewAPI(&pWinCtx->newAPI)) /* Only run when using the "old" Windows API. */ + { + LogFlowFuncEnter(); + + HWND hViewer = GetClipboardViewer(); + + /* Re-register ourselves in the clipboard chain if our last ping + * timed out or there seems to be no valid chain. */ + if (!hViewer || pWinCtx->oldAPI.fCBChainPingInProcess) + { + SharedClipboardWinChainRemove(pWinCtx); + SharedClipboardWinChainAdd(pWinCtx); + } + + /* Start a new ping by passing a dummy WM_CHANGECBCHAIN to be + * processed by ourselves to the chain. */ + pWinCtx->oldAPI.fCBChainPingInProcess = TRUE; + + hViewer = GetClipboardViewer(); + if (hViewer) + SendMessageCallback(hViewer, WM_CHANGECBCHAIN, (WPARAM)pWinCtx->hWndNextInChain, (LPARAM)pWinCtx->hWndNextInChain, + SharedClipboardWinChainPingProc, (ULONG_PTR)pWinCtx); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Announces a clipboard format to the Windows clipboard. + * + * The actual rendering (setting) of the clipboard data will be done later with + * a separate WM_RENDERFORMAT message. + * + * @returns VBox status code. VERR_NOT_SUPPORTED if the format is not supported / handled. + * @param pWinCtx Windows context to use. + * @param fFormats Clipboard format(s) to announce. + */ +static int sharedClipboardWinAnnounceFormats(PSHCLWINCTX pWinCtx, SHCLFORMATS fFormats) +{ + LogFunc(("fFormats=0x%x\n", fFormats)); + + /* + * Set the clipboard formats. + */ + static struct + { + uint32_t fVBoxFormat; + UINT uWinFormat; + const char *pszWinFormat; + const char *pszLog; + } s_aFormats[] = + { + { VBOX_SHCL_FMT_UNICODETEXT, CF_UNICODETEXT, NULL, "CF_UNICODETEXT" }, + { VBOX_SHCL_FMT_BITMAP, CF_DIB, NULL, "CF_DIB" }, + { VBOX_SHCL_FMT_HTML, 0, SHCL_WIN_REGFMT_HTML, "SHCL_WIN_REGFMT_HTML" }, + }; + unsigned cSuccessfullySet = 0; + SHCLFORMATS fFormatsLeft = fFormats; + int rc = VINF_SUCCESS; + for (uintptr_t i = 0; i < RT_ELEMENTS(s_aFormats) && fFormatsLeft != 0; i++) + { + if (fFormatsLeft & s_aFormats[i].fVBoxFormat) + { + LogFunc(("%s\n", s_aFormats[i].pszLog)); + fFormatsLeft &= ~s_aFormats[i].fVBoxFormat; + + /* Reg format if needed: */ + UINT uWinFormat = s_aFormats[i].uWinFormat; + if (!uWinFormat) + { + uWinFormat = RegisterClipboardFormat(s_aFormats[i].pszWinFormat); + AssertContinue(uWinFormat != 0); + } + + /* Tell the clipboard we've got data upon a request. We check the + last error here as hClip will be NULL even on success (despite + what MSDN says). */ + SetLastError(NO_ERROR); + HANDLE hClip = SetClipboardData(uWinFormat, NULL); + DWORD dwErr = GetLastError(); + if (dwErr == NO_ERROR || hClip != NULL) + cSuccessfullySet++; + else + { + AssertMsgFailed(("%s/%u: %u\n", s_aFormats[i].pszLog, uWinFormat, dwErr)); + rc = RTErrConvertFromWin32(dwErr); + } + } + } + + /* + * Consider setting anything a success, converting any error into + * informational status. Unsupport error only happens if all formats + * were unsupported. + */ + if (cSuccessfullySet > 0) + { + pWinCtx->hWndClipboardOwnerUs = GetClipboardOwner(); + if (RT_FAILURE(rc)) + rc = -rc; + } + else if (RT_SUCCESS(rc) && fFormatsLeft != 0) + { + LogFunc(("Unsupported formats: %#x (%#x)\n", fFormatsLeft, fFormats)); + rc = VERR_NOT_SUPPORTED; + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Opens the clipboard, clears it, announces @a fFormats and closes it. + * + * The actual rendering (setting) of the clipboard data will be done later with + * a separate WM_RENDERFORMAT message. + * + * @returns VBox status code. VERR_NOT_SUPPORTED if the format is not supported / handled. + * @param pWinCtx Windows context to use. + * @param fFormats Clipboard format(s) to announce. + * @param hWnd The window handle to use as owner. + */ +int SharedClipboardWinClearAndAnnounceFormats(PSHCLWINCTX pWinCtx, SHCLFORMATS fFormats, HWND hWnd) +{ + int rc = SharedClipboardWinOpen(hWnd); + if (RT_SUCCESS(rc)) + { + SharedClipboardWinClear(); + + rc = sharedClipboardWinAnnounceFormats(pWinCtx, fFormats); + Assert(pWinCtx->hWndClipboardOwnerUs == hWnd || pWinCtx->hWndClipboardOwnerUs == NULL); + + SharedClipboardWinClose(); + } + return rc; +} + +/** + * Writes (places) clipboard data into the Windows clipboard. + * + * @returns VBox status code. + * @param cfFormat Windows clipboard format to write data for. + * @param pvData Pointer to actual clipboard data to write. + * @param cbData Size (in bytes) of actual clipboard data to write. + * + * @note ASSUMES that the clipboard has already been opened. + */ +int SharedClipboardWinDataWrite(UINT cfFormat, void *pvData, uint32_t cbData) +{ + AssertPtrReturn(pvData, VERR_INVALID_POINTER); + AssertReturn (cbData, VERR_INVALID_PARAMETER); + + int rc = VINF_SUCCESS; + + HANDLE hMem = GlobalAlloc(GMEM_DDESHARE | GMEM_MOVEABLE, cbData); + + LogFlowFunc(("hMem=%p\n", hMem)); + + if (hMem) + { + void *pMem = GlobalLock(hMem); + + LogFlowFunc(("pMem=%p, GlobalSize=%zu\n", pMem, GlobalSize(hMem))); + + if (pMem) + { + LogFlowFunc(("Setting data\n")); + + memcpy(pMem, pvData, cbData); + + /* The memory must be unlocked before inserting to the Clipboard. */ + GlobalUnlock(hMem); + + /* 'hMem' contains the host clipboard data. + * size is 'cb' and format is 'format'. + */ + HANDLE hClip = SetClipboardData(cfFormat, hMem); + + LogFlowFunc(("hClip=%p\n", hClip)); + + if (hClip) + { + /* The hMem ownership has gone to the system. Nothing to do. */ + } + else + rc = RTErrConvertFromWin32(GetLastError()); + } + else + rc = VERR_ACCESS_DENIED; + + GlobalFree(hMem); + } + else + rc = RTErrConvertFromWin32(GetLastError()); + + if (RT_FAILURE(rc)) + LogFunc(("Setting clipboard data failed with %Rrc\n", rc)); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + +/** + * Creates an Shared Clipboard transfer by announcing transfer data (via IDataObject) to Windows. + * + * This creates the necessary IDataObject + IStream implementations and initiates the actual transfers required for getting + * the meta data. Whether or not the actual (file++) transfer(s) are happening is up to the user (at some point) later then. + * + * @returns VBox status code. + * @param pWinCtx Windows context to use. + * @param pTransferCtxCtx Transfer contextto use. + * @param pTransfer Shared Clipboard transfer to use. + */ +int SharedClipboardWinTransferCreate(PSHCLWINCTX pWinCtx, PSHCLTRANSFER pTransfer) +{ + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + + LogFlowFunc(("pWinCtx=%p\n", pWinCtx)); + + AssertReturn(pTransfer->pvUser == NULL, VERR_WRONG_ORDER); + + /* Make sure to enter the critical section before setting the clipboard data, as otherwise WM_CLIPBOARDUPDATE + * might get called *before* we had the opportunity to set pWinCtx->hWndClipboardOwnerUs below. */ + int rc = RTCritSectEnter(&pWinCtx->CritSect); + if (RT_SUCCESS(rc)) + { + SharedClipboardWinTransferCtx *pWinURITransferCtx = new SharedClipboardWinTransferCtx(); + if (pWinURITransferCtx) + { + pTransfer->pvUser = pWinURITransferCtx; + pTransfer->cbUser = sizeof(SharedClipboardWinTransferCtx); + + pWinURITransferCtx->pDataObj = new SharedClipboardWinDataObject(pTransfer); + if (pWinURITransferCtx->pDataObj) + { + rc = pWinURITransferCtx->pDataObj->Init(); + if (RT_SUCCESS(rc)) + { + SharedClipboardWinClose(); + /* Note: Clipboard must be closed first before calling OleSetClipboard(). */ + + /** @todo There is a potential race between SharedClipboardWinClose() and OleSetClipboard(), + * where another application could own the clipboard (open), and thus the call to + * OleSetClipboard() will fail. Needs (better) fixing. */ + HRESULT hr = S_OK; + + for (unsigned uTries = 0; uTries < 3; uTries++) + { + hr = OleSetClipboard(pWinURITransferCtx->pDataObj); + if (SUCCEEDED(hr)) + { + Assert(OleIsCurrentClipboard(pWinURITransferCtx->pDataObj) == S_OK); /* Sanity. */ + + /* + * Calling OleSetClipboard() changed the clipboard owner, which in turn will let us receive + * a WM_CLIPBOARDUPDATE message. To not confuse ourselves with our own clipboard owner changes, + * save a new window handle and deal with it in WM_CLIPBOARDUPDATE. + */ + pWinCtx->hWndClipboardOwnerUs = GetClipboardOwner(); + + LogFlowFunc(("hWndClipboardOwnerUs=%p\n", pWinCtx->hWndClipboardOwnerUs)); + break; + } + + LogFlowFunc(("Failed with %Rhrc (try %u/3)\n", hr, uTries + 1)); + RTThreadSleep(500); /* Wait a bit. */ + } + + if (FAILED(hr)) + { + rc = VERR_ACCESS_DENIED; /** @todo Fudge; fix this. */ + LogRel(("Shared Clipboard: Failed with %Rhrc when setting data object to clipboard\n", hr)); + } + } + } + else + rc = VERR_NO_MEMORY; + } + else + rc = VERR_NO_MEMORY; + + int rc2 = RTCritSectLeave(&pWinCtx->CritSect); + AssertRC(rc2); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Destroys implementation-specific data for an Shared Clipboard transfer. + * + * @param pWinCtx Windows context to use. + * @param pTransfer Shared Clipboard transfer to create implementation-specific data for. + */ +void SharedClipboardWinTransferDestroy(PSHCLWINCTX pWinCtx, PSHCLTRANSFER pTransfer) +{ + RT_NOREF(pWinCtx); + + if (!pTransfer) + return; + + LogFlowFuncEnter(); + + if (pTransfer->pvUser) + { + Assert(pTransfer->cbUser == sizeof(SharedClipboardWinTransferCtx)); + SharedClipboardWinTransferCtx *pWinURITransferCtx = (SharedClipboardWinTransferCtx *)pTransfer->pvUser; + Assert(pWinURITransferCtx); + + if (pWinURITransferCtx->pDataObj) + { + delete pWinURITransferCtx->pDataObj; + pWinURITransferCtx->pDataObj = NULL; + } + + delete pWinURITransferCtx; + + pTransfer->pvUser = NULL; + pTransfer->cbUser = 0; + } +} + +/** + * Retrieves the roots for a transfer by opening the clipboard and getting the clipboard data + * as string list (CF_HDROP), assigning it to the transfer as roots then. + * + * @returns VBox status code. + * @param pWinCtx Windows context to use. + * @param pTransfer Transfer to get roots for. + */ +int SharedClipboardWinGetRoots(PSHCLWINCTX pWinCtx, PSHCLTRANSFER pTransfer) +{ + AssertPtrReturn(pWinCtx, VERR_INVALID_POINTER); + AssertPtrReturn(pTransfer, VERR_INVALID_POINTER); + + Assert(ShClTransferGetSource(pTransfer) == SHCLSOURCE_LOCAL); /* Sanity. */ + + int rc = SharedClipboardWinOpen(pWinCtx->hWnd); + if (RT_SUCCESS(rc)) + { + /* The data data in CF_HDROP format, as the files are locally present and don't need to be + * presented as a IDataObject or IStream. */ + HANDLE hClip = hClip = GetClipboardData(CF_HDROP); + if (hClip) + { + HDROP hDrop = (HDROP)GlobalLock(hClip); + if (hDrop) + { + char *papszList = NULL; + uint32_t cbList; + rc = SharedClipboardWinDropFilesToStringList((DROPFILES *)hDrop, &papszList, &cbList); + + GlobalUnlock(hClip); + + if (RT_SUCCESS(rc)) + { + rc = ShClTransferRootsSet(pTransfer, + papszList, cbList + 1 /* Include termination */); + RTStrFree(papszList); + } + } + else + LogRel(("Shared Clipboard: Unable to lock clipboard data, last error: %ld\n", GetLastError())); + } + else + LogRel(("Shared Clipboard: Unable to retrieve clipboard data from clipboard (CF_HDROP), last error: %ld\n", + GetLastError())); + + SharedClipboardWinClose(); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Converts a DROPFILES (HDROP) structure to a string list, separated by \r\n. + * Does not do any locking on the input data. + * + * @returns VBox status code. + * @param pDropFiles Pointer to DROPFILES structure to convert. + * @param papszList Where to store the allocated string list. + * @param pcbList Where to store the size (in bytes) of the allocated string list. + */ +int SharedClipboardWinDropFilesToStringList(DROPFILES *pDropFiles, char **papszList, uint32_t *pcbList) +{ + AssertPtrReturn(pDropFiles, VERR_INVALID_POINTER); + AssertPtrReturn(papszList, VERR_INVALID_POINTER); + AssertPtrReturn(pcbList, VERR_INVALID_POINTER); + + /* Do we need to do Unicode stuff? */ + const bool fUnicode = RT_BOOL(pDropFiles->fWide); + + /* Get the offset of the file list. */ + Assert(pDropFiles->pFiles >= sizeof(DROPFILES)); + + /* Note: This is *not* pDropFiles->pFiles! DragQueryFile only + * will work with the plain storage medium pointer! */ + HDROP hDrop = (HDROP)(pDropFiles); + + int rc = VINF_SUCCESS; + + /* First, get the file count. */ + /** @todo Does this work on Windows 2000 / NT4? */ + char *pszFiles = NULL; + uint32_t cchFiles = 0; + UINT cFiles = DragQueryFile(hDrop, UINT32_MAX /* iFile */, NULL /* lpszFile */, 0 /* cchFile */); + + LogFlowFunc(("Got %RU16 file(s), fUnicode=%RTbool\n", cFiles, fUnicode)); + + for (UINT i = 0; i < cFiles; i++) + { + UINT cchFile = DragQueryFile(hDrop, i /* File index */, NULL /* Query size first */, 0 /* cchFile */); + Assert(cchFile); + + if (RT_FAILURE(rc)) + break; + + char *pszFileUtf8 = NULL; /* UTF-8 version. */ + UINT cchFileUtf8 = 0; + if (fUnicode) + { + /* Allocate enough space (including terminator). */ + WCHAR *pwszFile = (WCHAR *)RTMemAlloc((cchFile + 1) * sizeof(WCHAR)); + if (pwszFile) + { + const UINT cwcFileUtf16 = DragQueryFileW(hDrop, i /* File index */, + pwszFile, cchFile + 1 /* Include terminator */); + + AssertMsg(cwcFileUtf16 == cchFile, ("cchFileUtf16 (%RU16) does not match cchFile (%RU16)\n", + cwcFileUtf16, cchFile)); + RT_NOREF(cwcFileUtf16); + + rc = RTUtf16ToUtf8(pwszFile, &pszFileUtf8); + if (RT_SUCCESS(rc)) + { + cchFileUtf8 = (UINT)strlen(pszFileUtf8); + Assert(cchFileUtf8); + } + + RTMemFree(pwszFile); + } + else + rc = VERR_NO_MEMORY; + } + else /* ANSI */ + { + /* Allocate enough space (including terminator). */ + char *pszFileANSI = (char *)RTMemAlloc((cchFile + 1) * sizeof(char)); + UINT cchFileANSI = 0; + if (pszFileANSI) + { + cchFileANSI = DragQueryFileA(hDrop, i /* File index */, + pszFileANSI, cchFile + 1 /* Include terminator */); + + AssertMsg(cchFileANSI == cchFile, ("cchFileANSI (%RU16) does not match cchFile (%RU16)\n", + cchFileANSI, cchFile)); + + /* Convert the ANSI codepage to UTF-8. */ + rc = RTStrCurrentCPToUtf8(&pszFileUtf8, pszFileANSI); + if (RT_SUCCESS(rc)) + { + cchFileUtf8 = (UINT)strlen(pszFileUtf8); + } + } + else + rc = VERR_NO_MEMORY; + } + + if (RT_SUCCESS(rc)) + { + LogFlowFunc(("\tFile: %s (cchFile=%RU16)\n", pszFileUtf8, cchFileUtf8)); + + LogRel2(("Shared Clipboard: Adding file '%s' to transfer\n", pszFileUtf8)); + + rc = RTStrAAppendExN(&pszFiles, 1 /* cPairs */, pszFileUtf8, strlen(pszFileUtf8)); + cchFiles += (uint32_t)strlen(pszFileUtf8); + } + + if (pszFileUtf8) + RTStrFree(pszFileUtf8); + + if (RT_FAILURE(rc)) + { + LogFunc(("Error handling file entry #%u, rc=%Rrc\n", i, rc)); + break; + } + + /* Add separation between filenames. + * Note: Also do this for the last element of the list. */ + rc = RTStrAAppendExN(&pszFiles, 1 /* cPairs */, "\r\n", 2 /* Bytes */); + if (RT_SUCCESS(rc)) + cchFiles += 2; /* Include \r\n */ + } + + if (RT_SUCCESS(rc)) + { + cchFiles += 1; /* Add string termination. */ + uint32_t cbFiles = cchFiles * sizeof(char); /* UTF-8. */ + + LogFlowFunc(("cFiles=%u, cchFiles=%RU32, cbFiles=%RU32, pszFiles=0x%p\n", + cFiles, cchFiles, cbFiles, pszFiles)); + + *papszList = pszFiles; + *pcbList = cbFiles; + } + else + { + if (pszFiles) + RTStrFree(pszFiles); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +#endif /* VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS */ + diff --git a/src/VBox/GuestHost/SharedClipboard/clipboard-x11.cpp b/src/VBox/GuestHost/SharedClipboard/clipboard-x11.cpp new file mode 100644 index 00000000..5e57d85e --- /dev/null +++ b/src/VBox/GuestHost/SharedClipboard/clipboard-x11.cpp @@ -0,0 +1,2428 @@ +/** @file + * Shared Clipboard: Common X11 code. + */ + +/* + * Copyright (C) 2006-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +/* Note: to automatically run regression tests on the Shared Clipboard, + * execute the tstClipboardGH-X11 testcase. If you often make changes to the + * clipboard code, adding the line + * + * OTHERS += $(PATH_tstClipboardGH-X11)/tstClipboardGH-X11.run + * + * to LocalConfig.kmk will cause the tests to be run every time the code is + * changed. + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_SHARED_CLIPBOARD + +#include + +#include +#include +#include + +#ifdef RT_OS_SOLARIS +#include +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS +# include +# include +# include +#endif + +#include +#include + +#include +#include +#include +#include + +/** Own macro for declaring function visibility / linkage based on whether this + * code runs as part of test cases or not. */ +#ifdef TESTCASE +# define SHCL_X11_DECL(x) x +#else +# define SHCL_X11_DECL(x) static x +#endif + + +/********************************************************************************************************************************* +* Externals * +*********************************************************************************************************************************/ +#ifdef TESTCASE +extern void tstThreadScheduleCall(void (*proc)(void *, void *), void *client_data); +extern void tstClipRequestData(SHCLX11CTX* pCtx, SHCLX11FMTIDX target, void *closure); +extern void tstRequestTargets(SHCLX11CTX* pCtx); +#endif + + +/********************************************************************************************************************************* +* Prototypes * +*********************************************************************************************************************************/ +class formats; +SHCL_X11_DECL(Atom) clipGetAtom(PSHCLX11CTX pCtx, const char *pszName); +SHCL_X11_DECL(void) clipQueryX11Targets(PSHCLX11CTX pCtx); + +static int clipInitInternal(PSHCLX11CTX pCtx); +static void clipUninitInternal(PSHCLX11CTX pCtx); + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ + +/** + * The table maps X11 names to data formats + * and to the corresponding VBox clipboard formats. + */ +SHCL_X11_DECL(SHCLX11FMTTABLE) g_aFormats[] = +{ + { "INVALID", SHCLX11FMT_INVALID, VBOX_SHCL_FMT_NONE }, + + { "UTF8_STRING", SHCLX11FMT_UTF8, VBOX_SHCL_FMT_UNICODETEXT }, + { "text/plain;charset=UTF-8", SHCLX11FMT_UTF8, VBOX_SHCL_FMT_UNICODETEXT }, + { "text/plain;charset=utf-8", SHCLX11FMT_UTF8, VBOX_SHCL_FMT_UNICODETEXT }, + { "STRING", SHCLX11FMT_TEXT, VBOX_SHCL_FMT_UNICODETEXT }, + { "TEXT", SHCLX11FMT_TEXT, VBOX_SHCL_FMT_UNICODETEXT }, + { "text/plain", SHCLX11FMT_TEXT, VBOX_SHCL_FMT_UNICODETEXT }, + + { "text/html", SHCLX11FMT_HTML, VBOX_SHCL_FMT_HTML }, + { "text/html;charset=utf-8", SHCLX11FMT_HTML, VBOX_SHCL_FMT_HTML }, + { "application/x-moz-nativehtml", SHCLX11FMT_HTML, VBOX_SHCL_FMT_HTML }, + + { "image/bmp", SHCLX11FMT_BMP, VBOX_SHCL_FMT_BITMAP }, + { "image/x-bmp", SHCLX11FMT_BMP, VBOX_SHCL_FMT_BITMAP }, + { "image/x-MS-bmp", SHCLX11FMT_BMP, VBOX_SHCL_FMT_BITMAP }, + /** @todo Inkscape exports image/png but not bmp... */ + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + { "text/uri-list", SHCLX11FMT_URI_LIST, VBOX_SHCL_FMT_URI_LIST }, + { "x-special/gnome-copied-files", SHCLX11FMT_URI_LIST, VBOX_SHCL_FMT_URI_LIST }, + { "x-special/nautilus-clipboard", SHCLX11FMT_URI_LIST, VBOX_SHCL_FMT_URI_LIST }, + { "application/x-kde-cutselection", SHCLX11FMT_URI_LIST, VBOX_SHCL_FMT_URI_LIST }, + /** @todo Anything else we need to add here? */ + /** @todo Add Wayland / Weston support. */ +#endif +}; + + +#ifdef TESTCASE +# ifdef RT_OS_SOLARIS_10 +char XtStrings [] = ""; +WidgetClassRec* applicationShellWidgetClass; +char XtShellStrings [] = ""; +int XmbTextPropertyToTextList( + Display* /* display */, + XTextProperty* /* text_prop */, + char*** /* list_return */, + int* /* count_return */ +) +{ + return 0; +} +# else +const char XtStrings [] = ""; +_WidgetClassRec* applicationShellWidgetClass; +const char XtShellStrings [] = ""; +# endif /* RT_OS_SOLARIS_10 */ +#endif /* TESTCASE */ + + +/********************************************************************************************************************************* +* Defines * +*********************************************************************************************************************************/ + +#define SHCL_MAX_X11_FORMATS RT_ELEMENTS(g_aFormats) + + +/********************************************************************************************************************************* +* Internal structures * +*********************************************************************************************************************************/ + +/** + * A structure containing information about where to store a request + * for the X11 clipboard contents. + */ +typedef struct _CLIPREADX11CBREQ +{ + /** The format VBox would like the data in. */ + SHCLFORMAT uFmtVBox; + /** The format we requested from X11. */ + SHCLX11FMTIDX idxFmtX11; + /** The clipboard context this request is associated with. */ + SHCLX11CTX *pCtx; + /** The request structure passed in from the backend. */ + CLIPREADCBREQ *pReq; +} CLIPREADX11CBREQ; + + + +#ifdef TESTCASE +/** + * Return the max. number of elements in the X11 format table. + * Used by the testing code in tstClipboardGH-X11.cpp + * which cannot use RT_ELEMENTS(g_aFormats) directly. + * + * @return size_t The number of elements in the g_aFormats array. + */ +SHCL_X11_DECL(size_t) clipReportMaxX11Formats(void) +{ + return (RT_ELEMENTS(g_aFormats)); +} +#endif + +/** + * Returns the atom corresponding to a supported X11 format. + * + * @returns Found atom to the corresponding X11 format. + * @param pCtx The X11 clipboard context to use. + * @param uFmtIdx Format index to look up atom for. + */ +static Atom clipAtomForX11Format(PSHCLX11CTX pCtx, SHCLX11FMTIDX uFmtIdx) +{ + AssertReturn(uFmtIdx < RT_ELEMENTS(g_aFormats), 0); + return clipGetAtom(pCtx, g_aFormats[uFmtIdx].pcszAtom); +} + +/** + * Returns the SHCLX11FMT corresponding to a supported X11 format. + * + * @return SHCLX11FMT for a specific format index. + * @param uFmtIdx Format index to look up SHCLX11CLIPFMT for. + */ +SHCL_X11_DECL(SHCLX11FMT) clipRealFormatForX11Format(SHCLX11FMTIDX uFmtIdx) +{ + AssertReturn(uFmtIdx < RT_ELEMENTS(g_aFormats), SHCLX11FMT_INVALID); + return g_aFormats[uFmtIdx].enmFmtX11; +} + +/** + * Returns the VBox format corresponding to a supported X11 format. + * + * @return SHCLFORMAT for a specific format index. + * @param uFmtIdx Format index to look up VBox format for. + */ +static SHCLFORMAT clipVBoxFormatForX11Format(SHCLX11FMTIDX uFmtIdx) +{ + AssertReturn(uFmtIdx < RT_ELEMENTS(g_aFormats), VBOX_SHCL_FMT_NONE); + return g_aFormats[uFmtIdx].uFmtVBox; +} + +/** + * Looks up the X11 format matching a given X11 atom. + * + * @returns The format on success, NIL_CLIPX11FORMAT on failure. + * @param pCtx The X11 clipboard context to use. + * @param atomFormat Atom to look up X11 format for. + */ +static SHCLX11FMTIDX clipFindX11FormatByAtom(PSHCLX11CTX pCtx, Atom atomFormat) +{ + for (unsigned i = 0; i < RT_ELEMENTS(g_aFormats); ++i) + if (clipAtomForX11Format(pCtx, i) == atomFormat) + { + LogFlowFunc(("Returning index %u for atom '%s'\n", i, g_aFormats[i].pcszAtom)); + return i; + } + return NIL_CLIPX11FORMAT; +} + +/** + * Enumerates supported X11 clipboard formats corresponding to given VBox formats. + * + * @returns The next matching X11 format index in the list, or NIL_CLIPX11FORMAT if there are no more. + * @param uFormatsVBox VBox formats to enumerate supported X11 clipboard formats for. + * @param lastFmtIdx The value returned from the last call of this function. + * Use NIL_CLIPX11FORMAT to start the enumeration. + */ +static SHCLX11FMTIDX clipEnumX11Formats(SHCLFORMATS uFormatsVBox, + SHCLX11FMTIDX lastFmtIdx) +{ + for (unsigned i = lastFmtIdx + 1; i < RT_ELEMENTS(g_aFormats); ++i) + { + if (uFormatsVBox & clipVBoxFormatForX11Format(i)) + return i; + } + + return NIL_CLIPX11FORMAT; +} + +/** + * Array of structures for mapping Xt widgets to context pointers. We + * need this because the widget clipboard callbacks do not pass user data. + */ +static struct +{ + /** Pointer to widget we want to associate the context with. */ + Widget pWidget; + /** Pointer to X11 context associated with the widget. */ + PSHCLX11CTX pCtx; +} g_aContexts[VBOX_SHARED_CLIPBOARD_X11_CONNECTIONS_MAX]; + +/** + * Registers a new X11 clipboard context. + * + * @returns VBox status code. + * @param pCtx The X11 clipboard context to use. + */ +static int clipRegisterContext(PSHCLX11CTX pCtx) +{ + AssertPtrReturn(pCtx, VERR_INVALID_PARAMETER); + + bool fFound = false; + + Widget pWidget = pCtx->pWidget; + AssertReturn(pWidget != NULL, VERR_INVALID_PARAMETER); + + for (unsigned i = 0; i < RT_ELEMENTS(g_aContexts); ++i) + { + AssertReturn( (g_aContexts[i].pWidget != pWidget) + && (g_aContexts[i].pCtx != pCtx), VERR_WRONG_ORDER); + if (g_aContexts[i].pWidget == NULL && !fFound) + { + AssertReturn(g_aContexts[i].pCtx == NULL, VERR_INTERNAL_ERROR); + g_aContexts[i].pWidget = pWidget; + g_aContexts[i].pCtx = pCtx; + fFound = true; + } + } + + return fFound ? VINF_SUCCESS : VERR_OUT_OF_RESOURCES; +} + +/** + * Unregister an X11 clipboard context. + * + * @param pCtx The X11 clipboard context to use. + */ +static void clipUnregisterContext(PSHCLX11CTX pCtx) +{ + AssertPtrReturnVoid(pCtx); + + Widget pWidget = pCtx->pWidget; + AssertPtrReturnVoid(pWidget); + + bool fFound = false; + for (unsigned i = 0; i < RT_ELEMENTS(g_aContexts); ++i) + { + Assert(!fFound || g_aContexts[i].pWidget != pWidget); + if (g_aContexts[i].pWidget == pWidget) + { + Assert(g_aContexts[i].pCtx != NULL); + g_aContexts[i].pWidget = NULL; + g_aContexts[i].pCtx = NULL; + fFound = true; + } + } +} + +/** + * Finds a X11 clipboard context for a specific X11 widget. + * + * @returns Pointer to associated X11 clipboard context if found, or NULL if not found. + * @param pWidget X11 widget to return X11 clipboard context for. + */ +static PSHCLX11CTX clipLookupContext(Widget pWidget) +{ + AssertPtrReturn(pWidget, NULL); + + for (unsigned i = 0; i < RT_ELEMENTS(g_aContexts); ++i) + { + if (g_aContexts[i].pWidget == pWidget) + { + Assert(g_aContexts[i].pCtx != NULL); + return g_aContexts[i].pCtx; + } + } + + return NULL; +} + +/** + * Converts an atom name string to an X11 atom, looking it up in a cache before asking the server. + * + * @returns Found X11 atom. + * @param pCtx The X11 clipboard context to use. + * @param pcszName Name of atom to return atom for. + */ +SHCL_X11_DECL(Atom) clipGetAtom(PSHCLX11CTX pCtx, const char *pcszName) +{ + AssertPtrReturn(pcszName, None); + return XInternAtom(XtDisplay(pCtx->pWidget), pcszName, False); +} + +/** String written to the wakeup pipe. */ +#define WAKE_UP_STRING "WakeUp!" +/** Length of the string written. */ +#define WAKE_UP_STRING_LEN ( sizeof(WAKE_UP_STRING) - 1 ) + +/** + * Schedules a function call to run on the Xt event thread by passing it to + * the application context as a 0ms timeout and waking up the event loop by + * writing to the wakeup pipe which it monitors. + */ +static int clipThreadScheduleCall(PSHCLX11CTX pCtx, + void (*proc)(void *, void *), + void *client_data) +{ + LogFlowFunc(("proc=%p, client_data=%p\n", proc, client_data)); + +#ifndef TESTCASE + AssertReturn(pCtx, VERR_INVALID_POINTER); + AssertReturn(pCtx->pAppContext, VERR_INVALID_POINTER); + + XtAppAddTimeOut(pCtx->pAppContext, 0, (XtTimerCallbackProc)proc, + (XtPointer)client_data); + ssize_t cbWritten = write(pCtx->wakeupPipeWrite, WAKE_UP_STRING, WAKE_UP_STRING_LEN); + Assert(cbWritten == WAKE_UP_STRING_LEN); + RT_NOREF(cbWritten); +#else + RT_NOREF(pCtx); + tstThreadScheduleCall(proc, client_data); +#endif + + LogFlowFuncLeaveRC(VINF_SUCCESS); + return VINF_SUCCESS; +} + +/** + * Reports the formats currently supported by the X11 clipboard to VBox. + * + * @note Runs in Xt event thread. + * + * @param pCtx The X11 clipboard context to use. + */ +static void clipReportFormatsToVBox(PSHCLX11CTX pCtx) +{ + SHCLFORMATS vboxFmt = clipVBoxFormatForX11Format(pCtx->idxFmtText); + vboxFmt |= clipVBoxFormatForX11Format(pCtx->idxFmtBmp); + vboxFmt |= clipVBoxFormatForX11Format(pCtx->idxFmtHTML); +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + vboxFmt |= clipVBoxFormatForX11Format(pCtx->idxFmtURI); +#endif + + LogFlowFunc(("idxFmtText=%u ('%s'), idxFmtBmp=%u ('%s'), idxFmtHTML=%u ('%s')", + pCtx->idxFmtText, g_aFormats[pCtx->idxFmtText].pcszAtom, + pCtx->idxFmtBmp, g_aFormats[pCtx->idxFmtBmp].pcszAtom, + pCtx->idxFmtHTML, g_aFormats[pCtx->idxFmtHTML].pcszAtom)); +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + LogFlowFunc((", idxFmtURI=%u ('%s')", pCtx->idxFmtURI, g_aFormats[pCtx->idxFmtURI].pcszAtom)); +#endif + LogFlow((" -> vboxFmt=%#x\n", vboxFmt)); + +#ifdef LOG_ENABLED + char *pszFmts = ShClFormatsToStrA(vboxFmt); + AssertPtrReturnVoid(pszFmts); + LogRel2(("Shared Clipboard: X11 reported available VBox formats '%s'\n", pszFmts)); + RTStrFree(pszFmts); +#endif + + pCtx->Callbacks.pfnReportFormats(pCtx->pFrontend, vboxFmt, NULL /* pvUser */); +} + +/** + * Forgets which formats were previously in the X11 clipboard. Called when we + * grab the clipboard. + * + * @param pCtx The X11 clipboard context to use. + */ +static void clipResetX11Formats(PSHCLX11CTX pCtx) +{ + LogFlowFuncEnter(); + + pCtx->idxFmtText = 0; + pCtx->idxFmtBmp = 0; + pCtx->idxFmtHTML = 0; +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + pCtx->idxFmtURI = 0; +#endif +} + +/** + * Tells VBox that X11 currently has nothing in its clipboard. + * + * @param pCtx The X11 clipboard context to use. + */ +SHCL_X11_DECL(void) clipReportEmpty(PSHCLX11CTX pCtx) +{ + clipResetX11Formats(pCtx); + clipReportFormatsToVBox(pCtx); +} + +/** + * Go through an array of X11 clipboard targets to see if they contain a text + * format we can support, and if so choose the ones we prefer (e.g. we like + * UTF-8 better than plain text). + * + * @return Index to supported X clipboard format. + * @param pCtx The X11 clipboard context to use. + * @param paIdxFmtTargets The list of targets. + * @param cTargets The size of the list in @a pTargets. + */ +SHCL_X11_DECL(SHCLX11FMTIDX) clipGetTextFormatFromTargets(PSHCLX11CTX pCtx, + SHCLX11FMTIDX *paIdxFmtTargets, + size_t cTargets) +{ + AssertPtrReturn(pCtx, NIL_CLIPX11FORMAT); + AssertReturn(RT_VALID_PTR(paIdxFmtTargets) || cTargets == 0, NIL_CLIPX11FORMAT); + + SHCLX11FMTIDX idxFmtText = NIL_CLIPX11FORMAT; + SHCLX11FMT fmtTextX11 = SHCLX11FMT_INVALID; + for (unsigned i = 0; i < cTargets; ++i) + { + SHCLX11FMTIDX idxFmt = paIdxFmtTargets[i]; + if (idxFmt != NIL_CLIPX11FORMAT) + { + if ( (clipVBoxFormatForX11Format(idxFmt) == VBOX_SHCL_FMT_UNICODETEXT) + && fmtTextX11 < clipRealFormatForX11Format(idxFmt)) + { + fmtTextX11 = clipRealFormatForX11Format(idxFmt); + idxFmtText = idxFmt; + } + } + } + return idxFmtText; +} + +/** + * Goes through an array of X11 clipboard targets to see if they contain a bitmap + * format we can support, and if so choose the ones we prefer (e.g. we like + * BMP better than PNG because we don't have to convert). + * + * @return Supported X clipboard format. + * @param pCtx The X11 clipboard context to use. + * @param paIdxFmtTargets The list of targets. + * @param cTargets The size of the list in @a pTargets. + */ +static SHCLX11FMTIDX clipGetBitmapFormatFromTargets(PSHCLX11CTX pCtx, + SHCLX11FMTIDX *paIdxFmtTargets, + size_t cTargets) +{ + AssertPtrReturn(pCtx, NIL_CLIPX11FORMAT); + AssertReturn(RT_VALID_PTR(paIdxFmtTargets) || cTargets == 0, NIL_CLIPX11FORMAT); + + SHCLX11FMTIDX idxFmtBmp = NIL_CLIPX11FORMAT; + SHCLX11FMT fmtBmpX11 = SHCLX11FMT_INVALID; + for (unsigned i = 0; i < cTargets; ++i) + { + SHCLX11FMTIDX idxFmt = paIdxFmtTargets[i]; + if (idxFmt != NIL_CLIPX11FORMAT) + { + if ( (clipVBoxFormatForX11Format(idxFmt) == VBOX_SHCL_FMT_BITMAP) + && fmtBmpX11 < clipRealFormatForX11Format(idxFmt)) + { + fmtBmpX11 = clipRealFormatForX11Format(idxFmt); + idxFmtBmp = idxFmt; + } + } + } + return idxFmtBmp; +} + +/** + * Goes through an array of X11 clipboard targets to see if they contain a HTML + * format we can support, and if so choose the ones we prefer. + * + * @return Supported X clipboard format. + * @param pCtx The X11 clipboard context to use. + * @param paIdxFmtTargets The list of targets. + * @param cTargets The size of the list in @a pTargets. + */ +static SHCLX11FMTIDX clipGetHtmlFormatFromTargets(PSHCLX11CTX pCtx, + SHCLX11FMTIDX *paIdxFmtTargets, + size_t cTargets) +{ + AssertPtrReturn(pCtx, NIL_CLIPX11FORMAT); + AssertReturn(RT_VALID_PTR(paIdxFmtTargets) || cTargets == 0, NIL_CLIPX11FORMAT); + + SHCLX11FMTIDX idxFmtHTML = NIL_CLIPX11FORMAT; + SHCLX11FMT fmxHTMLX11 = SHCLX11FMT_INVALID; + for (unsigned i = 0; i < cTargets; ++i) + { + SHCLX11FMTIDX idxFmt = paIdxFmtTargets[i]; + if (idxFmt != NIL_CLIPX11FORMAT) + { + if ( (clipVBoxFormatForX11Format(idxFmt) == VBOX_SHCL_FMT_HTML) + && fmxHTMLX11 < clipRealFormatForX11Format(idxFmt)) + { + fmxHTMLX11 = clipRealFormatForX11Format(idxFmt); + idxFmtHTML = idxFmt; + } + } + } + return idxFmtHTML; +} + +# ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS +/** + * Goes through an array of X11 clipboard targets to see if they contain an URI list + * format we can support, and if so choose the ones we prefer. + * + * @return Supported X clipboard format. + * @param pCtx The X11 clipboard context to use. + * @param paIdxFmtTargets The list of targets. + * @param cTargets The size of the list in @a pTargets. + */ +static SHCLX11FMTIDX clipGetURIListFormatFromTargets(PSHCLX11CTX pCtx, + SHCLX11FMTIDX *paIdxFmtTargets, + size_t cTargets) +{ + AssertPtrReturn(pCtx, NIL_CLIPX11FORMAT); + AssertReturn(RT_VALID_PTR(paIdxFmtTargets) || cTargets == 0, NIL_CLIPX11FORMAT); + + SHCLX11FMTIDX idxFmtURI = NIL_CLIPX11FORMAT; + SHCLX11FMT fmtURIX11 = SHCLX11FMT_INVALID; + for (unsigned i = 0; i < cTargets; ++i) + { + SHCLX11FMTIDX idxFmt = paIdxFmtTargets[i]; + if (idxFmt != NIL_CLIPX11FORMAT) + { + if ( (clipVBoxFormatForX11Format(idxFmt) == VBOX_SHCL_FMT_URI_LIST) + && fmtURIX11 < clipRealFormatForX11Format(idxFmt)) + { + fmtURIX11 = clipRealFormatForX11Format(idxFmt); + idxFmtURI = idxFmt; + } + } + } + return idxFmtURI; +} +# endif /* VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS */ + +/** + * Goes through an array of X11 clipboard targets to see if we can support any + * of them and if relevant to choose the ones we prefer (e.g. we like Utf8 + * better than plain text). + * + * @param pCtx The X11 clipboard context to use. + * @param paIdxFmtTargets The list of targets. + * @param cTargets The size of the list in @a pTargets. + */ +static void clipGetFormatsFromTargets(PSHCLX11CTX pCtx, + SHCLX11FMTIDX *paIdxFmtTargets, size_t cTargets) +{ + AssertPtrReturnVoid(pCtx); + AssertPtrReturnVoid(paIdxFmtTargets); + + SHCLX11FMTIDX idxFmtText = clipGetTextFormatFromTargets(pCtx, paIdxFmtTargets, cTargets); + if (pCtx->idxFmtText != idxFmtText) + pCtx->idxFmtText = idxFmtText; + + pCtx->idxFmtBmp = SHCLX11FMT_INVALID; /* not yet supported */ /** @todo r=andy Check this. */ + SHCLX11FMTIDX idxFmtBmp = clipGetBitmapFormatFromTargets(pCtx, paIdxFmtTargets, cTargets); + if (pCtx->idxFmtBmp != idxFmtBmp) + pCtx->idxFmtBmp = idxFmtBmp; + + SHCLX11FMTIDX idxFmtHTML = clipGetHtmlFormatFromTargets(pCtx, paIdxFmtTargets, cTargets); + if (pCtx->idxFmtHTML != idxFmtHTML) + pCtx->idxFmtHTML = idxFmtHTML; + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + SHCLX11FMTIDX idxFmtURI = clipGetURIListFormatFromTargets(pCtx, paIdxFmtTargets, cTargets); + if (pCtx->idxFmtURI != idxFmtURI) + pCtx->idxFmtURI = idxFmtURI; +#endif +} + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_XT_BUSY +DECLINLINE(bool) clipGetXtBusy(PSHCLX11CTX pCtx) +{ + LogFlowFunc(("fXtBusy=%RTbool, fXtNeedsUpdate=%RTbool\n", pCtx->fXtBusy, pCtx->fXtNeedsUpdate)); + return pCtx->fXtBusy; +} + +DECLINLINE(bool) clipGetXtNeedsUpdate(PSHCLX11CTX pCtx) +{ + LogFlowFunc(("fXtBusy=%RTbool, fXtNeedsUpdate=%RTbool\n", pCtx->fXtBusy, pCtx->fXtNeedsUpdate)); + return pCtx->fXtNeedsUpdate; +} + +DECLINLINE(bool) clipSetXtBusy(PSHCLX11CTX pCtx, bool fBusy) +{ + pCtx->fXtBusy = fBusy; + LogFlowFunc(("fXtBusy=%RTbool, fXtNeedsUpdate=%RTbool\n", pCtx->fXtBusy, pCtx->fXtNeedsUpdate)); + return pCtx->fXtBusy; +} + +DECLINLINE(bool) clipSetXtNeedsUpdate(PSHCLX11CTX pCtx, bool fNeedsUpdate) +{ + pCtx->fXtNeedsUpdate = fNeedsUpdate; + LogFlowFunc(("fXtBusy=%RTbool, fXtNeedsUpdate=%RTbool\n", pCtx->fXtBusy, pCtx->fXtNeedsUpdate)); + return pCtx->fXtNeedsUpdate; +} +#endif /* VBOX_WITH_SHARED_CLIPBOARD_XT_BUSY */ + +/** + * Updates the context's information about targets currently supported by X11, + * based on an array of X11 atoms. + * + * @param pCtx The X11 clipboard context to use. + * @param pTargets The array of atoms describing the targets supported. + * @param cTargets The size of the array @a pTargets. + */ +SHCL_X11_DECL(void) clipUpdateX11Targets(PSHCLX11CTX pCtx, SHCLX11FMTIDX *paIdxFmtTargets, size_t cTargets) +{ + LogFlowFuncEnter(); + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_XT_BUSY + clipSetXtBusy(pCtx, false); + if (clipGetXtNeedsUpdate(pCtx)) + { + /* We may already be out of date. */ + clipSetXtNeedsUpdate(pCtx, false); + clipQueryX11Targets(pCtx); + return; + } +#endif + + if (paIdxFmtTargets == NULL) + { + /* No data available */ + clipReportEmpty(pCtx); + return; + } + + clipGetFormatsFromTargets(pCtx, paIdxFmtTargets, cTargets); + clipReportFormatsToVBox(pCtx); +} + +/** + * Notifies the VBox clipboard about available data formats ("targets" on X11), + * based on the information obtained from the X11 clipboard. + * + * @note Callback installed by clipQueryX11Targets() for XtGetSelectionValue(). + * @note This function is treated as API glue, and as such is not part of any + * unit test. So keep it simple, be paranoid and log everything. + */ +SHCL_X11_DECL(void) clipQueryX11TargetsCallback(Widget widget, XtPointer pClient, + Atom * /* selection */, Atom *atomType, + XtPointer pValue, long unsigned int *pcLen, + int *piFormat) +{ + RT_NOREF(piFormat); + + PSHCLX11CTX pCtx = reinterpret_cast(pClient); + + LogFlowFunc(("pValue=%p, *pcLen=%u, *atomType=%d%s\n", + pValue, *pcLen, *atomType, *atomType == XT_CONVERT_FAIL ? " (XT_CONVERT_FAIL)" : "")); + + Atom *pAtoms = (Atom *)pValue; + + unsigned cFormats = *pcLen; + + LogRel2(("Shared Clipboard: Querying X11 formats ...\n")); + LogRel2(("Shared Clipboard: %u X11 formats were found\n", cFormats)); + + SHCLX11FMTIDX *paIdxFmt = NULL; + if ( cFormats + && pValue + && (*atomType != XT_CONVERT_FAIL /* time out */)) + { + /* Allocated array to hold the format indices. */ + paIdxFmt = (SHCLX11FMTIDX *)RTMemAllocZ(cFormats * sizeof(SHCLX11FMTIDX)); + } + +#if !defined(TESTCASE) + if (pValue) + { + for (unsigned i = 0; i < cFormats; ++i) + { + if (pAtoms[i]) + { + char *pszName = XGetAtomName(XtDisplay(widget), pAtoms[i]); + LogRel2(("Shared Clipboard: Found X11 format '%s'\n", pszName)); + XFree(pszName); + } + else + LogFunc(("Found empty target\n")); + } + } +#endif + + if (paIdxFmt) + { + for (unsigned i = 0; i < cFormats; ++i) + { + for (unsigned j = 0; j < RT_ELEMENTS(g_aFormats); ++j) + { + Atom target = XInternAtom(XtDisplay(widget), + g_aFormats[j].pcszAtom, False); + if (*(pAtoms + i) == target) + paIdxFmt[i] = j; + } +#if !defined(TESTCASE) + if (paIdxFmt[i] != SHCLX11FMT_INVALID) + LogRel2(("Shared Clipboard: Reporting X11 format '%s'\n", g_aFormats[paIdxFmt[i]].pcszAtom)); +#endif + } + } + else + LogFunc(("Reporting empty targets (none reported or allocation failure)\n")); + + clipUpdateX11Targets(pCtx, paIdxFmt, cFormats); + RTMemFree(paIdxFmt); + + XtFree(reinterpret_cast(pValue)); +} + +/** + * Queries the current formats ("targets") of the X11 clipboard ("CLIPBOARD"). + * + * @param pCtx The X11 clipboard context to use. + */ +SHCL_X11_DECL(void) clipQueryX11Targets(PSHCLX11CTX pCtx) +{ +#ifndef TESTCASE + +# ifdef VBOX_WITH_SHARED_CLIPBOARD_XT_BUSY + if (clipGetXtBusy(pCtx)) + { + clipSetXtNeedsUpdate(pCtx, true); + return; + } + clipSetXtBusy(pCtx, true); +# endif + + XtGetSelectionValue(pCtx->pWidget, + clipGetAtom(pCtx, "CLIPBOARD"), + clipGetAtom(pCtx, "TARGETS"), + clipQueryX11TargetsCallback, pCtx, + CurrentTime); +#else + tstRequestTargets(pCtx); +#endif +} + +typedef struct +{ + int type; /* event base */ + unsigned long serial; + Bool send_event; + Display *display; + Window window; + int subtype; + Window owner; + Atom selection; + Time timestamp; + Time selection_timestamp; +} XFixesSelectionNotifyEvent; + +#ifndef TESTCASE +/** + * Waits until an event arrives and handle it if it is an XFIXES selection + * event, which Xt doesn't know about. + * + * @param pCtx The X11 clipboard context to use. + */ +static void clipPeekEventAndDoXFixesHandling(PSHCLX11CTX pCtx) +{ + union + { + XEvent event; + XFixesSelectionNotifyEvent fixes; + } event = { { 0 } }; + + if (XtAppPeekEvent(pCtx->pAppContext, &event.event)) + { + if ( (event.event.type == pCtx->fixesEventBase) + && (event.fixes.owner != XtWindow(pCtx->pWidget))) + { + if ( (event.fixes.subtype == 0 /* XFixesSetSelectionOwnerNotify */) + && (event.fixes.owner != 0)) + clipQueryX11Targets(pCtx); + else + clipReportEmpty(pCtx); + } + } +} + +/** + * The main loop of our X11 event thread. + * + * @returns VBox status code. + * @param hThreadSelf Associated thread handle. + * @param pvUser Pointer to the X11 clipboard context to use. + */ +static DECLCALLBACK(int) clipThreadMain(RTTHREAD hThreadSelf, void *pvUser) +{ + PSHCLX11CTX pCtx = (PSHCLX11CTX)pvUser; + AssertPtr(pCtx); + + LogFlowFunc(("pCtx=%p\n", pCtx)); + + bool fSignalled = false; /* Whether we have signalled the parent already or not. */ + + int rc = clipInitInternal(pCtx); + if (RT_SUCCESS(rc)) + { + rc = clipRegisterContext(pCtx); + if (RT_SUCCESS(rc)) + { + if (pCtx->fGrabClipboardOnStart) + clipQueryX11Targets(pCtx); + + pCtx->fThreadStarted = true; + + /* We're now ready to run, tell parent. */ + int rc2 = RTThreadUserSignal(hThreadSelf); + AssertRC(rc2); + + fSignalled = true; + + while (XtAppGetExitFlag(pCtx->pAppContext) == FALSE) + { + clipPeekEventAndDoXFixesHandling(pCtx); + XtAppProcessEvent(pCtx->pAppContext, XtIMAll); + } + + LogRel(("Shared Clipboard: X11 event thread exiting\n")); + + clipUnregisterContext(pCtx); + } + else + { + LogRel(("Shared Clipboard: unable to register clip context: %Rrc\n", rc)); + } + + clipUninitInternal(pCtx); + } + + if (!fSignalled) /* Signal parent if we didn't do so yet. */ + { + int rc2 = RTThreadUserSignal(hThreadSelf); + AssertRC(rc2); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Worker function for stopping the clipboard which runs on the event + * thread. + * + * @param pvUserData Pointer to the X11 clipboard context to use. + */ +static void clipThreadSignalStop(void *pvUserData, void *) +{ + PSHCLX11CTX pCtx = (PSHCLX11CTX)pvUserData; + + /* This might mean that we are getting stopped twice. */ + Assert(pCtx->pWidget != NULL); + + /* Set the termination flag to tell the Xt event loop to exit. We + * reiterate that any outstanding requests from the X11 event loop to + * the VBox part *must* have returned before we do this. */ + XtAppSetExitFlag(pCtx->pAppContext); +} + +/** + * Sets up the XFixes library and load the XFixesSelectSelectionInput symbol. + */ +static int clipLoadXFixes(Display *pDisplay, PSHCLX11CTX pCtx) +{ + int rc; + + void *hFixesLib = dlopen("libXfixes.so.1", RTLD_LAZY); + if (!hFixesLib) + hFixesLib = dlopen("libXfixes.so.2", RTLD_LAZY); + if (!hFixesLib) + hFixesLib = dlopen("libXfixes.so.3", RTLD_LAZY); + if (!hFixesLib) + hFixesLib = dlopen("libXfixes.so.4", RTLD_LAZY); + if (hFixesLib) + { + /* For us, a NULL function pointer is a failure */ + pCtx->fixesSelectInput = (void (*)(Display *, Window, Atom, long unsigned int)) + (uintptr_t)dlsym(hFixesLib, "XFixesSelectSelectionInput"); + if (pCtx->fixesSelectInput) + { + int dummy1 = 0; + int dummy2 = 0; + if (XQueryExtension(pDisplay, "XFIXES", &dummy1, &pCtx->fixesEventBase, &dummy2) != 0) + { + if (pCtx->fixesEventBase >= 0) + { + rc = VINF_SUCCESS; + } + else + { + LogRel(("Shared Clipboard: fixesEventBase is less than zero: %d\n", pCtx->fixesEventBase)); + rc = VERR_NOT_SUPPORTED; + } + } + else + { + LogRel(("Shared Clipboard: XQueryExtension failed\n")); + rc = VERR_NOT_SUPPORTED; + } + } + else + { + LogRel(("Shared Clipboard: Symbol XFixesSelectSelectionInput not found!\n")); + rc = VERR_NOT_SUPPORTED; + } + } + else + { + LogRel(("Shared Clipboard: libxFixes.so.* not found!\n")); + rc = VERR_NOT_SUPPORTED; + } + return rc; +} + +/** + * This is the callback which is scheduled when data is available on the + * wakeup pipe. It simply reads all data from the pipe. + * + * @param pvUserData Pointer to the X11 clipboard context to use. + */ +static void clipThreadDrainWakeupPipe(XtPointer pvUserData, int *, XtInputId *) +{ + LogFlowFuncEnter(); + + PSHCLX11CTX pCtx = (PSHCLX11CTX)pvUserData; + char acBuf[WAKE_UP_STRING_LEN]; + + while (read(pCtx->wakeupPipeRead, acBuf, sizeof(acBuf)) > 0) {} +} +#endif /* !TESTCASE */ + +/** + * X11-specific initialisation for the Shared Clipboard. + * + * Note: Must be called from the thread serving the Xt stuff. + * + * @returns VBox status code. + * @param pCtx The X11 clipboard context to init. + */ +static int clipInitInternal(PSHCLX11CTX pCtx) +{ + LogFlowFunc(("pCtx=%p\n", pCtx)); + + /* Make sure we are thread safe. */ + XtToolkitThreadInitialize(); + + /* + * Set up the Clipboard application context and main window. We call all + * these functions directly instead of calling XtOpenApplication() so + * that we can fail gracefully if we can't get an X11 display. + */ + XtToolkitInitialize(); + + int rc = VINF_SUCCESS; + + Assert(pCtx->pAppContext == NULL); /* No nested initialization. */ + pCtx->pAppContext = XtCreateApplicationContext(); + if (pCtx->pAppContext == NULL) + { + LogRel(("Shared Clipboard: Failed to create Xt application context\n")); + return VERR_NOT_SUPPORTED; /** @todo Fudge! */ + } + + /* Create a window and make it a clipboard viewer. */ + int cArgc = 0; + char *pcArgv = 0; + Display *pDisplay = XtOpenDisplay(pCtx->pAppContext, 0, 0, "VBoxShCl", 0, 0, &cArgc, &pcArgv); + if (pDisplay == NULL) + { + LogRel(("Shared Clipboard: Failed to connect to the X11 clipboard - the window system may not be running\n")); + rc = VERR_NOT_SUPPORTED; + } + +#ifndef TESTCASE + if (RT_SUCCESS(rc)) + { + rc = clipLoadXFixes(pDisplay, pCtx); + if (RT_FAILURE(rc)) + LogRel(("Shared Clipboard: Failed to load the XFIXES extension\n")); + } +#endif + + if (RT_SUCCESS(rc)) + { + pCtx->pWidget = XtVaAppCreateShell(0, "VBoxShCl", + applicationShellWidgetClass, + pDisplay, XtNwidth, 1, XtNheight, + 1, NULL); + if (pCtx->pWidget == NULL) + { + LogRel(("Shared Clipboard: Failed to create Xt app shell\n")); + rc = VERR_NO_MEMORY; /** @todo r=andy Improve this. */ + } + else + { +#ifndef TESTCASE + if (!XtAppAddInput(pCtx->pAppContext, pCtx->wakeupPipeRead, + (XtPointer) XtInputReadMask, + clipThreadDrainWakeupPipe, (XtPointer) pCtx)) + { + LogRel(("Shared Clipboard: Failed to add input to Xt app context\n")); + rc = VERR_ACCESS_DENIED; /** @todo r=andy Improve this. */ + } +#endif + } + } + + if (RT_SUCCESS(rc)) + { + XtSetMappedWhenManaged(pCtx->pWidget, false); + XtRealizeWidget(pCtx->pWidget); + +#ifndef TESTCASE + /* Enable clipboard update notification. */ + pCtx->fixesSelectInput(pDisplay, XtWindow(pCtx->pWidget), + clipGetAtom(pCtx, "CLIPBOARD"), + 7 /* All XFixes*Selection*NotifyMask flags */); +#endif + } + + if (RT_FAILURE(rc)) + { + LogRel(("Shared Clipboard: Initialisation failed: %Rrc\n", rc)); + clipUninitInternal(pCtx); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * X11-specific uninitialisation for the Shared Clipboard. + * + * Note: Must be called from the thread serving the Xt stuff. + * + * @param pCtx The X11 clipboard context to uninit. + */ +static void clipUninitInternal(PSHCLX11CTX pCtx) +{ + AssertPtrReturnVoid(pCtx); + + LogFlowFunc(("pCtx=%p\n", pCtx)); + + if (pCtx->pWidget) + { + /* Valid widget + invalid appcontext = bug. But don't return yet. */ + AssertPtr(pCtx->pAppContext); + + XtDestroyWidget(pCtx->pWidget); + pCtx->pWidget = NULL; + } + + if (pCtx->pAppContext) + { + XtDestroyApplicationContext(pCtx->pAppContext); + pCtx->pAppContext = NULL; + } + + LogFlowFuncLeaveRC(VINF_SUCCESS); +} + +/** + * Sets the callback table, internal version. + * + * @param pCtx The clipboard context. + * @param pCallbacks Callback table to set. If NULL, the current callback table will be cleared. + */ +static void shClX11SetCallbacksInternal(PSHCLX11CTX pCtx, PSHCLCALLBACKS pCallbacks) +{ + if (pCallbacks) + { + memcpy(&pCtx->Callbacks, pCallbacks, sizeof(SHCLCALLBACKS)); + } + else + RT_ZERO(pCtx->Callbacks); +} + +/** + * Sets the callback table. + * + * @param pCtx The clipboard context. + * @param pCallbacks Callback table to set. If NULL, the current callback table will be cleared. + */ +void ShClX11SetCallbacks(PSHCLX11CTX pCtx, PSHCLCALLBACKS pCallbacks) +{ + shClX11SetCallbacksInternal(pCtx, pCallbacks); +} + +/** + * Initializes a X11 context of the Shared Clipboard. + * + * @returns VBox status code. + * @param pCtx The clipboard context to initialize. + * @param pCallbacks Callback table to use. + * @param pParent Parent context to use. + * @param fHeadless Whether the code runs in a headless environment or not. + */ +int ShClX11Init(PSHCLX11CTX pCtx, PSHCLCALLBACKS pCallbacks, PSHCLCONTEXT pParent, bool fHeadless) +{ + AssertPtrReturn(pCtx, VERR_INVALID_POINTER); + + LogFlowFunc(("pCtx=%p\n", pCtx)); + + int rc = VINF_SUCCESS; + + RT_BZERO(pCtx, sizeof(SHCLX11CTX)); + + if (fHeadless) + { + /* + * If we don't find the DISPLAY environment variable we assume that + * we are not connected to an X11 server. Don't actually try to do + * this then, just fail silently and report success on every call. + * This is important for VBoxHeadless. + */ + LogRel(("Shared Clipboard: X11 DISPLAY variable not set -- disabling clipboard sharing\n")); + } + + /* Install given callbacks. */ + shClX11SetCallbacksInternal(pCtx, pCallbacks); + + pCtx->fHaveX11 = !fHeadless; + pCtx->pFrontend = pParent; + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_XT_BUSY + pCtx->fXtBusy = false; + pCtx->fXtNeedsUpdate = false; +#endif + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS_HTTP + ShClTransferHttpServerInit(&pCtx->HttpCtx.HttpServer); +#endif + +#ifdef TESTCASE + if (RT_SUCCESS(rc)) + { + /** @todo The testcases currently do not utilize the threading code. So init stuff here. */ + rc = clipInitInternal(pCtx); + if (RT_SUCCESS(rc)) + rc = clipRegisterContext(pCtx); + } +#endif + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Destroys a Shared Clipboard X11 context. + * + * @param pCtx The X11 clipboard context to destroy. + */ +void ShClX11Destroy(PSHCLX11CTX pCtx) +{ + if (!pCtx) + return; + + LogFlowFunc(("pCtx=%p\n", pCtx)); + +#ifdef TESTCASE + /** @todo The testcases currently do not utilize the threading code. So uninit stuff here. */ + clipUnregisterContext(pCtx); + clipUninitInternal(pCtx); +#endif + + if (pCtx->fHaveX11) + { + /* We set this to NULL when the event thread exits. It really should + * have exited at this point, when we are about to unload the code from + * memory. */ + Assert(pCtx->pWidget == NULL); + } +} + +#ifndef TESTCASE +/** + * Starts our own Xt even thread for handling Shared Clipboard messages, extended version. + * + * @returns VBox status code. + * @param pCtx The X11 clipboard context to use. + * @param pszName Thread name to use. + * @param fGrab Whether we should try to grab the shared clipboard at once. + */ +int ShClX11ThreadStartEx(PSHCLX11CTX pCtx, const char *pszName, bool fGrab) +{ + AssertPtrReturn(pCtx, VERR_INVALID_POINTER); + + /* + * Immediately return if we are not connected to the X server. + */ + if (!pCtx->fHaveX11) + return VINF_SUCCESS; + + pCtx->fGrabClipboardOnStart = fGrab; + + clipResetX11Formats(pCtx); + + int rc; + + /* + * Create the pipes. + ** @todo r=andy Replace this with RTPipe API. + */ + int pipes[2]; + if (!pipe(pipes)) + { + pCtx->wakeupPipeRead = pipes[0]; + pCtx->wakeupPipeWrite = pipes[1]; + + if (!fcntl(pCtx->wakeupPipeRead, F_SETFL, O_NONBLOCK)) + { + rc = VINF_SUCCESS; + } + else + rc = RTErrConvertFromErrno(errno); + } + else + rc = RTErrConvertFromErrno(errno); + + if (RT_SUCCESS(rc)) + { + LogRel2(("Shared Clipboard: Starting X11 event thread ...\n")); + + rc = RTThreadCreate(&pCtx->Thread, clipThreadMain, pCtx, 0, + RTTHREADTYPE_IO, RTTHREADFLAGS_WAITABLE, pszName); + if (RT_SUCCESS(rc)) + rc = RTThreadUserWait(pCtx->Thread, RT_MS_30SEC /* msTimeout */); + + if (RT_FAILURE(rc)) + { + LogRel(("Shared Clipboard: Failed to start the X11 event thread with %Rrc\n", rc)); + clipUninitInternal(pCtx); + } + else + { + if (!pCtx->fThreadStarted) + { + LogRel(("Shared Clipboard: X11 event thread reported an error while starting\n")); + } + else + LogRel2(("Shared Clipboard: X11 event thread started\n")); + } + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Starts our own Xt even thread for handling Shared Clipboard messages. + * + * @returns VBox status code. + * @param pCtx The X11 clipboard context to use. + * @param fGrab Whether we should try to grab the shared clipboard at once. + */ +int ShClX11ThreadStart(PSHCLX11CTX pCtx, bool fGrab) +{ + return ShClX11ThreadStartEx(pCtx, "SHCLX11", fGrab); +} + +/** + * Stops the Shared Clipboard Xt even thread. + * + * @note Any requests from this object to get clipboard data from VBox + * *must* have completed or aborted before we are called, as + * otherwise the X11 event loop will still be waiting for the request + * to return and will not be able to terminate. + * + * @returns VBox status code. + * @param pCtx The X11 clipboard context to use. + */ +int ShClX11ThreadStop(PSHCLX11CTX pCtx) +{ + int rc; + /* + * Immediately return if we are not connected to the X server. + */ + if (!pCtx->fHaveX11) + return VINF_SUCCESS; + + LogRel2(("Shared Clipboard: Signalling the X11 event thread to stop\n")); + + /* Write to the "stop" pipe. */ + rc = clipThreadScheduleCall(pCtx, clipThreadSignalStop, (XtPointer)pCtx); + if (RT_FAILURE(rc)) + { + LogRel(("Shared Clipboard: cannot notify X11 event thread on shutdown with %Rrc\n", rc)); + return rc; + } + + LogRel2(("Shared Clipboard: Waiting for X11 event thread to stop ...\n")); + + int rcThread; + rc = RTThreadWait(pCtx->Thread, RT_MS_30SEC /* msTimeout */, &rcThread); + if (RT_SUCCESS(rc)) + rc = rcThread; + if (RT_SUCCESS(rc)) + { + if (pCtx->wakeupPipeRead != 0) + { + close(pCtx->wakeupPipeRead); + pCtx->wakeupPipeRead = 0; + } + + if (pCtx->wakeupPipeWrite != 0) + { + close(pCtx->wakeupPipeWrite); + pCtx->wakeupPipeWrite = 0; + } + } + + if (RT_SUCCESS(rc)) + { + LogRel2(("Shared Clipboard: X11 event thread stopped successfully\n")); + } + else + LogRel(("Shared Clipboard: Stopping X11 event thread failed with %Rrc\n", rc)); + + LogFlowFuncLeaveRC(rc); + return rc; +} +#endif /* !TESTCASE */ + +/** + * Returns the targets supported by VBox. + * + * This will return a list of atoms which tells the caller + * what kind of clipboard formats we support. + * + * @returns VBox status code. + * @param pCtx The X11 clipboard context to use. + * @param atomTypeReturn The type of the data we are returning. + * @param pValReturn A pointer to the data we are returning. This + * should be set to memory allocated by XtMalloc, + * which will be freed later by the Xt toolkit. + * @param pcLenReturn The length of the data we are returning. + * @param piFormatReturn The format (8bit, 16bit, 32bit) of the data we are + * returning. + * @note X11 backend code, called by the XtOwnSelection callback. + */ +static int clipCreateX11Targets(PSHCLX11CTX pCtx, Atom *atomTypeReturn, + XtPointer *pValReturn, + unsigned long *pcLenReturn, + int *piFormatReturn) +{ + const unsigned cFixedTargets = 3; /* See below. */ + + Atom *pAtomTargets = (Atom *)XtMalloc((SHCL_MAX_X11_FORMATS + cFixedTargets) * sizeof(Atom)); + if (!pAtomTargets) + return VERR_NO_MEMORY; + + unsigned cTargets = 0; + SHCLX11FMTIDX idxFmt = NIL_CLIPX11FORMAT; + do + { + idxFmt = clipEnumX11Formats(pCtx->vboxFormats, idxFmt); + if (idxFmt != NIL_CLIPX11FORMAT) + { + pAtomTargets[cTargets] = clipAtomForX11Format(pCtx, idxFmt); + ++cTargets; + } + } while (idxFmt != NIL_CLIPX11FORMAT); + + /* We always offer these fixed targets. */ + pAtomTargets[cTargets] = clipGetAtom(pCtx, "TARGETS"); + pAtomTargets[cTargets + 1] = clipGetAtom(pCtx, "MULTIPLE"); + pAtomTargets[cTargets + 2] = clipGetAtom(pCtx, "TIMESTAMP"); + + *atomTypeReturn = XA_ATOM; + *pValReturn = (XtPointer)pAtomTargets; + *pcLenReturn = cTargets + cFixedTargets; + *piFormatReturn = 32; + + LogFlowFunc(("cTargets=%u\n", cTargets + cFixedTargets)); + + return VINF_SUCCESS; +} + +/** + * Helper for ShClX11RequestDataForX11Callback() that will cache the data returned. + * + * @returns VBox status code. VERR_NO_DATA if no data available. + * @param pCtx The X11 clipboard context to use. + * @param uFmt Clipboard format to read data in. + * @param ppv Returns an allocated buffer with data read on success. + * Needs to be free'd with RTMemFree() by the caller. + * @param pcb Returns the amount of data read (in bytes) on success. + */ +static int shClX11RequestDataForX11CallbackHelper(PSHCLX11CTX pCtx, SHCLFORMAT uFmt, + void **ppv, uint32_t *pcb) +{ + AssertPtrReturn(pCtx, VERR_INVALID_POINTER); + AssertPtrReturn(ppv, VERR_INVALID_POINTER); + AssertPtrReturn(pcb, VERR_INVALID_POINTER); + + LogFlowFunc(("pCtx=%p, uFmt=%#x\n", pCtx, uFmt)); + + int rc = VINF_SUCCESS; + + void *pv = NULL; + uint32_t cb = 0; + + if (uFmt == VBOX_SHCL_FMT_UNICODETEXT) + { + if (pCtx->pvUnicodeCache == NULL) /** @todo r=andy Using string cache here? */ + rc = pCtx->Callbacks.pfnOnRequestDataFromSource(pCtx->pFrontend, uFmt, &pCtx->pvUnicodeCache, &pCtx->cbUnicodeCache, + NULL /* pvUser */); + if ( RT_SUCCESS(rc) + /* Catch misbehaving callbacks. */ + && pCtx->pvUnicodeCache + && pCtx->cbUnicodeCache) + { + pv = RTMemDup(pCtx->pvUnicodeCache, pCtx->cbUnicodeCache); + if (pv) + cb = pCtx->cbUnicodeCache; + else + rc = VERR_NO_MEMORY; + } + } + else + rc = pCtx->Callbacks.pfnOnRequestDataFromSource(pCtx->pFrontend, uFmt, &pv, &cb, NULL /* pvUser */); + + + /* Safey net in case the callbacks above misbehave + * (must return VERR_NO_DATA if no data available). */ + if ( RT_SUCCESS(rc) + && (pv == NULL || cb == 0)) + rc = VERR_NO_DATA; + + if (RT_SUCCESS(rc)) + { + *ppv = pv; + *pcb = cb; + } + + LogFlowFunc(("Returning pv=%p, cb=%RU32, rc=%Rrc\n", pv, cb, rc)); + return rc; +} + +/** + * Satisfies a request from X11 to convert the clipboard text to UTF-8 LF. + * + * @returns VBox status code. VERR_NO_DATA if no data was converted. + * @param pDisplay An X11 display structure, needed for conversions + * performed by Xlib. + * @param pv The text to be converted (UCS-2 with Windows EOLs). + * @param cb The length of the text in @cb in bytes. + * @param atomTypeReturn Where to store the atom for the type of the data + * we are returning. + * @param pValReturn Where to store the pointer to the data we are + * returning. This should be to memory allocated by + * XtMalloc, which will be freed by the Xt toolkit + * later. + * @param pcLenReturn Where to store the length of the data we are + * returning. + * @param piFormatReturn Where to store the bit width (8, 16, 32) of the + * data we are returning. + */ +static int clipConvertUtf16ToX11Data(Display *pDisplay, PRTUTF16 pwszSrc, + size_t cbSrc, Atom *atomTarget, + Atom *atomTypeReturn, + XtPointer *pValReturn, + unsigned long *pcLenReturn, + int *piFormatReturn) +{ + RT_NOREF(pDisplay); + AssertReturn(cbSrc % sizeof(RTUTF16) == 0, VERR_INVALID_PARAMETER); + + const size_t cwcSrc = cbSrc / sizeof(RTUTF16); + if (!cwcSrc) + return VERR_NO_DATA; + + /* This may slightly overestimate the space needed. */ + size_t chDst = 0; + int rc = ShClUtf16LenUtf8(pwszSrc, cwcSrc, &chDst); + if (RT_SUCCESS(rc)) + { + chDst++; /* Add space for terminator. */ + + char *pszDst = (char *)XtMalloc(chDst); + if (pszDst) + { + size_t cbActual = 0; + rc = ShClConvUtf16CRLFToUtf8LF(pwszSrc, cwcSrc, pszDst, chDst, &cbActual); + if (RT_SUCCESS(rc)) + { + *atomTypeReturn = *atomTarget; + *pValReturn = (XtPointer)pszDst; + *pcLenReturn = cbActual + 1 /* Include terminator */; + *piFormatReturn = 8; + } + } + else + rc = VERR_NO_MEMORY; + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Satisfies a request from X11 to convert the clipboard HTML fragment to UTF-8. We + * return null-terminated text, but can cope with non-null-terminated input. + * + * @returns VBox status code. + * @param pDisplay An X11 display structure, needed for conversions + * performed by Xlib. + * @param pv The text to be converted (UTF8 with Windows EOLs). + * @param cb The length of the text in @cb in bytes. + * @param atomTypeReturn Where to store the atom for the type of the data + * we are returning. + * @param pValReturn Where to store the pointer to the data we are + * returning. This should be to memory allocated by + * XtMalloc, which will be freed by the Xt toolkit later. + * @param pcLenReturn Where to store the length of the data we are returning. + * @param piFormatReturn Where to store the bit width (8, 16, 32) of the + * data we are returning. + */ +static int clipConvertHtmlToX11Data(Display *pDisplay, const char *pszSrc, + size_t cbSrc, Atom *atomTarget, + Atom *atomTypeReturn, + XtPointer *pValReturn, + unsigned long *pcLenReturn, + int *piFormatReturn) +{ + RT_NOREF(pDisplay, pValReturn); + + /* This may slightly overestimate the space needed. */ + LogFlowFunc(("Source: %s", pszSrc)); + + char *pszDest = (char *)XtMalloc(cbSrc); + if (pszDest == NULL) + return VERR_NO_MEMORY; + + memcpy(pszDest, pszSrc, cbSrc); + + *atomTypeReturn = *atomTarget; + *pValReturn = (XtPointer)pszDest; + *pcLenReturn = cbSrc; + *piFormatReturn = 8; + + return VINF_SUCCESS; +} + + +/** + * Does this atom correspond to one of the two selection types we support? + * + * @param pCtx The X11 clipboard context to use. + * @param selType The atom in question. + */ +static bool clipIsSupportedSelectionType(PSHCLX11CTX pCtx, Atom selType) +{ + return( (selType == clipGetAtom(pCtx, "CLIPBOARD")) + || (selType == clipGetAtom(pCtx, "PRIMARY"))); +} + +/** + * Removes a trailing nul character from a string by adjusting the string + * length. Some X11 applications don't like zero-terminated text... + * + * @param pText The text in question. + * @param pcText The length of the text, adjusted on return. + * @param format The format of the text. + */ +static void clipTrimTrailingNul(XtPointer pText, unsigned long *pcText, + SHCLX11FMT format) +{ + AssertPtrReturnVoid(pText); + AssertPtrReturnVoid(pcText); + AssertReturnVoid((format == SHCLX11FMT_UTF8) || (format == SHCLX11FMT_TEXT) || (format == SHCLX11FMT_HTML)); + + if (((char *)pText)[*pcText - 1] == '\0') + --(*pcText); +} + +static int clipConvertToX11Data(PSHCLX11CTX pCtx, Atom *atomTarget, + Atom *atomTypeReturn, + XtPointer *pValReturn, + unsigned long *pcLenReturn, + int *piFormatReturn) +{ + int rc = VERR_NOT_SUPPORTED; /* Play safe by default. */ + + SHCLX11FMTIDX idxFmtX11 = clipFindX11FormatByAtom(pCtx, *atomTarget); + SHCLX11FMT fmtX11 = clipRealFormatForX11Format(idxFmtX11); + + LogFlowFunc(("vboxFormats=0x%x, idxFmtX11=%u ('%s'), fmtX11=%u\n", + pCtx->vboxFormats, idxFmtX11, g_aFormats[idxFmtX11].pcszAtom, fmtX11)); + +#ifdef LOG_ENABLED + char *pszFmts = ShClFormatsToStrA(pCtx->vboxFormats); + AssertPtrReturn(pszFmts, VERR_NO_MEMORY); + LogRel2(("Shared Clipboard: Converting VBox formats '%s' to '%s' for X11\n", + pszFmts, g_aFormats[idxFmtX11].pcszAtom)); + RTStrFree(pszFmts); +#endif + + void *pv = NULL; + uint32_t cb = 0; + + if ( ( (fmtX11 == SHCLX11FMT_UTF8) + || (fmtX11 == SHCLX11FMT_TEXT) + ) + && (pCtx->vboxFormats & VBOX_SHCL_FMT_UNICODETEXT)) + { + rc = shClX11RequestDataForX11CallbackHelper(pCtx, VBOX_SHCL_FMT_UNICODETEXT, &pv, &cb); + if ( RT_SUCCESS(rc) + && ( (fmtX11 == SHCLX11FMT_UTF8) + || (fmtX11 == SHCLX11FMT_TEXT))) + { + rc = clipConvertUtf16ToX11Data(XtDisplay(pCtx->pWidget), + (PRTUTF16)pv, cb, atomTarget, + atomTypeReturn, pValReturn, + pcLenReturn, piFormatReturn); + } + + if (RT_SUCCESS(rc)) + clipTrimTrailingNul(*(XtPointer *)pValReturn, pcLenReturn, fmtX11); + + RTMemFree(pv); + } + else if ( (fmtX11 == SHCLX11FMT_BMP) + && (pCtx->vboxFormats & VBOX_SHCL_FMT_BITMAP)) + { + rc = shClX11RequestDataForX11CallbackHelper(pCtx, VBOX_SHCL_FMT_BITMAP, &pv, &cb); + if ( RT_SUCCESS(rc) + && (fmtX11 == SHCLX11FMT_BMP)) + { + /* Create a full BMP from it. */ + rc = ShClDibToBmp(pv, cb, (void **)pValReturn, + (size_t *)pcLenReturn); + } + + if (RT_SUCCESS(rc)) + { + *atomTypeReturn = *atomTarget; + *piFormatReturn = 8; + } + + RTMemFree(pv); + } + else if ( (fmtX11 == SHCLX11FMT_HTML) + && (pCtx->vboxFormats & VBOX_SHCL_FMT_HTML)) + { + rc = shClX11RequestDataForX11CallbackHelper(pCtx, VBOX_SHCL_FMT_HTML, &pv, &cb); + if (RT_SUCCESS(rc)) + { + /** + * The common VBox HTML encoding will be UTF-8. + * Before sending it to the X11 clipboard we have to convert it to UTF-8 first. + * + * Strange that we get UTF-16 from the X11 clipboard, but + * in same time we send UTF-8 to X11 clipboard and it works. + ** @todo r=andy Verify this. + */ + rc = clipConvertHtmlToX11Data(XtDisplay(pCtx->pWidget), + (const char*)pv, cb, atomTarget, + atomTypeReturn, pValReturn, + pcLenReturn, piFormatReturn); + if (RT_SUCCESS(rc)) + clipTrimTrailingNul(*(XtPointer *)pValReturn, pcLenReturn, fmtX11); + + RTMemFree(pv); + } + } +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + else if (fmtX11 == SHCLX11FMT_URI_LIST) + { + if (pCtx->vboxFormats & VBOX_SHCL_FMT_URI_LIST) + { + rc = shClX11RequestDataForX11CallbackHelper(pCtx, VBOX_SHCL_FMT_URI_LIST, &pv, &cb); + if (RT_SUCCESS(rc)) + { + void *pvDst = (void *)XtMalloc(cb); + if (pvDst) + { + memcpy(pvDst, pv, cb); + + *atomTypeReturn = *atomTarget; + *pValReturn = (XtPointer)pvDst; + *pcLenReturn = cb; + *piFormatReturn = 8; + } + else + rc = VERR_NO_MEMORY; + } + } + /* else not supported yet. */ + } +#endif + else + { + *atomTypeReturn = XT_CONVERT_FAIL; + *pValReturn = (XtPointer)NULL; + *pcLenReturn = 0; + *piFormatReturn = 0; + } + + if (RT_FAILURE(rc)) + { + char *pszFmts2 = ShClFormatsToStrA(pCtx->vboxFormats); + char *pszAtomName = XGetAtomName(XtDisplay(pCtx->pWidget), *atomTarget); + + LogRel(("Shared Clipboard: Converting VBox formats '%s' to '%s' for X11 (idxFmtX11=%u, fmtX11=%u, atomTarget='%s') failed, rc=%Rrc\n", + pszFmts2 ? pszFmts2 : "unknown", g_aFormats[idxFmtX11].pcszAtom, idxFmtX11, fmtX11, pszAtomName ? pszAtomName : "unknown", rc)); + + if (pszFmts2) + RTStrFree(pszFmts2); + if (pszAtomName) + XFree(pszAtomName); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Returns VBox's clipboard data for an X11 client. + * + * @note Callback for XtOwnSelection. + */ +static Boolean clipXtConvertSelectionProc(Widget widget, Atom *atomSelection, + Atom *atomTarget, + Atom *atomTypeReturn, + XtPointer *pValReturn, + unsigned long *pcLenReturn, + int *piFormatReturn) +{ + LogFlowFuncEnter(); + + PSHCLX11CTX pCtx = clipLookupContext(widget); + if (!pCtx) + return False; + + /* Is this the rigt selection (clipboard) we were asked for? */ + if (!clipIsSupportedSelectionType(pCtx, *atomSelection)) + return False; + + int rc; + if (*atomTarget == clipGetAtom(pCtx, "TARGETS")) + rc = clipCreateX11Targets(pCtx, atomTypeReturn, pValReturn, + pcLenReturn, piFormatReturn); + else + rc = clipConvertToX11Data(pCtx, atomTarget, atomTypeReturn, + pValReturn, pcLenReturn, piFormatReturn); + +#if 0 /** @todo Disabled -- crashes when running with tstClipboardGH-X11. */ + XSelectionRequestEvent* pReq = + XtGetSelectionRequest(widget, *atomSelection, (XtRequestId)NULL); + LogFlowFunc(("returning pVBoxWnd=%#x, ownerWnd=%#x, reqWnd=%#x, %RTbool, rc=%Rrc\n", + XtWindow(pCtx->pWidget), pReq->owner, pReq->requestor, RT_SUCCESS(rc), rc)); +#endif + return RT_SUCCESS(rc) ? True : False; +} + +static void clipXtConvertSelectionProcLose(Widget widget, Atom *atomSelection) +{ + RT_NOREF(widget, atomSelection); + LogFlowFuncEnter(); +} + +static void clipXtConvertSelectionProcDone(Widget widget, Atom *atomSelection, Atom *atomTarget) +{ + RT_NOREF(widget, atomSelection, atomTarget); + LogFlowFuncEnter(); +} + +/** + * Structure used to pass information about formats that VBox supports. + */ +typedef struct _CLIPNEWVBOXFORMATS +{ + /** Context information for the X11 clipboard. */ + PSHCLX11CTX pCtx; + /** Formats supported by VBox. */ + SHCLFORMATS Formats; +} CLIPNEWVBOXFORMATS, *PCLIPNEWVBOXFORMATS; + + + +/** + * Invalidates the local cache of the data in the VBox clipboard. + * + * @param pCtx The X11 clipboard context to use. + */ +static void clipInvalidateClipboardCache(PSHCLX11CTX pCtx) +{ + if (pCtx->pvUnicodeCache != NULL) + { + RTMemFree(pCtx->pvUnicodeCache); + pCtx->pvUnicodeCache = NULL; + } +} + +/** + * Takes possession of the X11 clipboard (and middle-button selection). + * + * @param pCtx The X11 clipboard context to use. + * @param uFormats Clipboard formats to set. + */ +static void clipGrabX11Clipboard(PSHCLX11CTX pCtx, SHCLFORMATS uFormats) +{ + LogFlowFuncEnter(); + + /** @ŧodo r=andy The docs say: "the value CurrentTime is not acceptable" here!? */ + if (XtOwnSelection(pCtx->pWidget, clipGetAtom(pCtx, "CLIPBOARD"), + CurrentTime, + clipXtConvertSelectionProc, clipXtConvertSelectionProcLose, clipXtConvertSelectionProcDone)) + { + pCtx->vboxFormats = uFormats; + + /* Grab the middle-button paste selection too. */ + XtOwnSelection(pCtx->pWidget, clipGetAtom(pCtx, "PRIMARY"), + CurrentTime, clipXtConvertSelectionProc, NULL, 0); +#ifndef TESTCASE + /* Xt suppresses these if we already own the clipboard, so send them + * ourselves. */ + XSetSelectionOwner(XtDisplay(pCtx->pWidget), + clipGetAtom(pCtx, "CLIPBOARD"), + XtWindow(pCtx->pWidget), CurrentTime); + XSetSelectionOwner(XtDisplay(pCtx->pWidget), + clipGetAtom(pCtx, "PRIMARY"), + XtWindow(pCtx->pWidget), CurrentTime); +#endif + } +} + +/** + * Worker function for ShClX11ReportFormatsToX11 which runs on the + * event thread. + * + * @param pvUserData Pointer to a CLIPNEWVBOXFORMATS structure containing + * information about the VBox formats available and the + * clipboard context data. Must be freed by the worker. + */ +static void ShClX11ReportFormatsToX11Worker(void *pvUserData, void * /* interval */) +{ + AssertPtrReturnVoid(pvUserData); + + CLIPNEWVBOXFORMATS *pFormats = (CLIPNEWVBOXFORMATS *)pvUserData; + + PSHCLX11CTX pCtx = pFormats->pCtx; + SHCLFORMATS fFormats = pFormats->Formats; + + RTMemFree(pFormats); + +#ifdef LOG_ENABLED + char *pszFmts = ShClFormatsToStrA(fFormats); + AssertPtrReturnVoid(pszFmts); + LogRel2(("Shared Clipboard: Reported available VBox formats %s to X11\n", pszFmts)); + RTStrFree(pszFmts); +#endif + + clipInvalidateClipboardCache(pCtx); + clipGrabX11Clipboard(pCtx, fFormats); + clipResetX11Formats(pCtx); + + LogFlowFuncLeave(); +} + +/** + * Announces new clipboard formats to the X11 clipboard. + * + * @returns VBox status code. + * @param pCtx Context data for the clipboard backend. + * @param uFormats Clipboard formats offered. + */ +int ShClX11ReportFormatsToX11(PSHCLX11CTX pCtx, SHCLFORMATS uFormats) +{ + /* + * Immediately return if we are not connected to the X server. + */ + if (!pCtx->fHaveX11) + return VINF_SUCCESS; + + int rc; + + /* This must be free'd by the worker callback. */ + PCLIPNEWVBOXFORMATS pFormats = (PCLIPNEWVBOXFORMATS)RTMemAlloc(sizeof(CLIPNEWVBOXFORMATS)); + if (pFormats) + { + pFormats->pCtx = pCtx; + pFormats->Formats = uFormats; + rc = clipThreadScheduleCall(pCtx, ShClX11ReportFormatsToX11Worker, + (XtPointer)pFormats); + if (RT_FAILURE(rc)) + RTMemFree(pFormats); + } + else + rc = VERR_NO_MEMORY; + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Converts the data obtained from the X11 clipboard to the required format, + * place it in the buffer supplied and signal that data has arrived. + * + * Converts the text obtained UTF-16LE with Windows EOLs. + * Converts full BMP data to DIB format. + */ +SHCL_X11_DECL(void) clipConvertDataFromX11Worker(void *pClient, void *pvSrc, unsigned cbSrc) +{ + CLIPREADX11CBREQ *pReq = (CLIPREADX11CBREQ *)pClient; + AssertPtrReturnVoid(pReq); + + LogFlowFunc(("pReq->uFmtVBox=%#x, pReq->idxFmtX11=%u, pReq->pCtx=%p\n", pReq->uFmtVBox, pReq->idxFmtX11, pReq->pCtx)); + + LogRel2(("Shared Clipboard: Converting X11 format '%s' to VBox format %#x\n", + g_aFormats[pReq->idxFmtX11].pcszAtom, pReq->uFmtVBox)); + + AssertPtr(pReq->pCtx); + Assert(pReq->uFmtVBox != VBOX_SHCL_FMT_NONE); /* Sanity. */ + + int rc = VINF_SUCCESS; + + void *pvDst = NULL; + size_t cbDst = 0; + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_XT_BUSY + PSHCLX11CTX pCtx = pReq->pCtx; + AssertPtr(pReq->pCtx); + clipSetXtBusy(pCtx, false); + if (clipGetXtNeedsUpdate(pCtx)) + clipQueryX11Targets(pCtx); +#endif + + /* If X11 clipboard buffer has no data, libXt can pass to XtGetSelectionValue() + * callback an empty string, in this case cbSrc is 0. */ + if (pvSrc == NULL || cbSrc == 0) + { + /* The clipboard selection may have changed before we could get it. */ + rc = VERR_NO_DATA; + } + else if (pReq->uFmtVBox == VBOX_SHCL_FMT_UNICODETEXT) + { + /* In which format is the clipboard data? */ + switch (clipRealFormatForX11Format(pReq->idxFmtX11)) + { + case SHCLX11FMT_UTF8: + RT_FALL_THROUGH(); + case SHCLX11FMT_TEXT: + { + size_t cwDst; + /* If we are given broken UTF-8, we treat it as Latin1. */ /** @todo BUGBUG Is this acceptable? */ + if (RT_SUCCESS(RTStrValidateEncodingEx((char *)pvSrc, cbSrc, 0))) + rc = ShClConvUtf8LFToUtf16CRLF((const char *)pvSrc, cbSrc, + (PRTUTF16 *)&pvDst, &cwDst); + else + rc = ShClConvLatin1LFToUtf16CRLF((char *)pvSrc, cbSrc, + (PRTUTF16 *)&pvDst, &cwDst); + if (RT_SUCCESS(rc)) + { + cwDst += 1 /* Include terminator */; + cbDst = cwDst * sizeof(RTUTF16); /* Convert RTUTF16 units to bytes. */ + } + break; + } + + default: + { + rc = VERR_INVALID_PARAMETER; + break; + } + } + } + else if (pReq->uFmtVBox == VBOX_SHCL_FMT_BITMAP) + { + /* In which format is the clipboard data? */ + switch (clipRealFormatForX11Format(pReq->idxFmtX11)) + { + case SHCLX11FMT_BMP: + { + const void *pDib; + size_t cbDibSize; + rc = ShClBmpGetDib((const void *)pvSrc, cbSrc, + &pDib, &cbDibSize); + if (RT_SUCCESS(rc)) + { + pvDst = RTMemAlloc(cbDibSize); + if (!pvDst) + rc = VERR_NO_MEMORY; + else + { + memcpy(pvDst, pDib, cbDibSize); + cbDst = cbDibSize; + } + } + break; + } + + default: + { + rc = VERR_INVALID_PARAMETER; + break; + } + } + } + else if (pReq->uFmtVBox == VBOX_SHCL_FMT_HTML) + { + /* In which format is the clipboard data? */ + switch (clipRealFormatForX11Format(pReq->idxFmtX11)) + { + case SHCLX11FMT_HTML: + { + /* + * The common VBox HTML encoding will be - UTF-8 + * because it more general for HTML formats then UTF-16 + * X11 clipboard returns UTF-16, so before sending it we should + * convert it to UTF-8. + */ + pvDst = NULL; + cbDst = 0; + + /* + * Some applications sends data in UTF-16, some in UTF-8, + * without indication it in MIME. + * + * In case of UTF-16, at least [Open|Libre] Office adds an byte order mark (0xfeff) + * at the start of the clipboard data. + */ + if ( cbSrc >= sizeof(RTUTF16) + && *(PRTUTF16)pvSrc == VBOX_SHCL_UTF16LEMARKER) + { + rc = ShClConvUtf16ToUtf8HTML((PRTUTF16)pvSrc, cbSrc / sizeof(RTUTF16), (char**)&pvDst, &cbDst); + if (RT_SUCCESS(rc)) + { + LogFlowFunc(("UTF-16 Unicode source (%u bytes):\n%ls\n\n", cbSrc, pvSrc)); + LogFlowFunc(("Byte Order Mark = %hx", ((PRTUTF16)pvSrc)[0])); + LogFlowFunc(("UTF-8 Unicode dest (%u bytes):\n%s\n\n", cbDst, pvDst)); + } + else + LogRel(("Shared Clipboard: Converting UTF-16 Unicode failed with %Rrc\n", rc)); + } + else /* Raw data. */ + { + pvDst = RTMemAllocZ(cbSrc + 1 /* '\0' */); + if(pvDst) + { + memcpy(pvDst, pvSrc, cbSrc); + cbDst = cbSrc + 1 /* '\0' */; + } + else + { + rc = VERR_NO_MEMORY; + break; + } + } + + rc = VINF_SUCCESS; + break; + } + + default: + { + rc = VERR_INVALID_PARAMETER; + break; + } + } + } +# ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + else if (pReq->uFmtVBox == VBOX_SHCL_FMT_URI_LIST) + { + /* In which format is the clipboard data? */ + switch (clipRealFormatForX11Format(pReq->idxFmtX11)) + { + case SHCLX11FMT_URI_LIST: + { + /* For URI lists we only accept valid UTF-8 encodings. */ + if (RT_SUCCESS(RTStrValidateEncodingEx((char *)pvSrc, cbSrc, 0))) + { + /* URI lists on X are strings separated with "\r\n". */ + RTCList lstRootEntries = RTCString((char *)pvSrc, cbSrc).split("\r\n"); + for (size_t i = 0; i < lstRootEntries.size(); ++i) + { + char *pszEntry = RTUriFilePath(lstRootEntries.at(i).c_str()); + AssertPtrBreakStmt(pszEntry, VERR_INVALID_PARAMETER); + + rc = RTStrAAppend((char **)&pvDst, "http://localhost"); + AssertRCBreakStmt(rc, VERR_NO_MEMORY); + cbDst += (uint32_t)strlen(pszEntry); + + + + /** @todo BUGBUG Fix port! */ + /** @todo Add port + UUID (virtual path). */ + + rc = RTStrAAppend((char **)&pvDst, pszEntry); + AssertRCBreakStmt(rc, VERR_NO_MEMORY); + cbDst += (uint32_t)strlen(pszEntry); + + LogFlowFunc(("URI list entry '%s'\n", (char *)pvDst)); + + rc = RTStrAAppend((char **)&pvDst, "\r\n"); + AssertRCBreakStmt(rc, VERR_NO_MEMORY); + cbDst += (uint32_t)strlen("\r\n"); + + RTStrFree(pszEntry); + } + + if (cbDst) + cbDst++; /* Include final (zero) termination. */ + + LogFlowFunc(("URI list: cbDst=%RU32\n", cbDst)); + } + else + rc = VERR_INVALID_PARAMETER; + break; + } + + default: + { + rc = VERR_INVALID_PARAMETER; + break; + } + } + } +# endif /* VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS */ + else + rc = VERR_NOT_SUPPORTED; + + if (RT_FAILURE(rc)) + LogRel(("Shared Clipboard: Converting X11 format '%s' (idxFmtX11=%u) to VBox format %#x failed, rc=%Rrc\n", + g_aFormats[pReq->idxFmtX11].pcszAtom, pReq->idxFmtX11, pReq->uFmtVBox, rc)); + + SHCLX11READDATAREQ SendData; + RT_ZERO(SendData); + SendData.pReq = pReq->pReq; + SendData.rcCompletion = rc; + + pCtx->Callbacks.pfnOnSendDataToDest(pReq->pCtx->pFrontend, pvDst, cbDst, &SendData); + + RTMemFree(pvDst); + RTMemFree(pReq); + + LogFlowFuncLeaveRC(rc); +} + +/** + * Converts the data obtained from the X11 clipboard to the required format, + * place it in the buffer supplied and signal that data has arrived. + * + * Converts the text obtained UTF-16LE with Windows EOLs. + * Converts full BMP data to DIB format. + */ +SHCL_X11_DECL(void) clipConvertDataFromX11(Widget widget, XtPointer pClient, + Atom * /* selection */, Atom *atomType, + XtPointer pvSrc, long unsigned int *pcLen, + int *piFormat) +{ + RT_NOREF(widget); + if (*atomType == XT_CONVERT_FAIL) /* Xt timeout */ + clipConvertDataFromX11Worker(pClient, NULL, 0); + else + { + CLIPREADX11CBREQ *pReq = (CLIPREADX11CBREQ *)pClient; + if (pReq->pCtx->Callbacks.pfnOnClipboardRead) + { + void *pvData = NULL; + size_t cbData = 0; + int rc = pReq->pCtx->Callbacks.pfnOnClipboardRead(pReq->pCtx->pFrontend, pReq->uFmtVBox, &pvData, &cbData, NULL); + if (RT_SUCCESS(rc)) + { + /* Feed to conversion worker. */ + clipConvertDataFromX11Worker(pClient, pvData, cbData); + RTMemFree(pvData); + } + else + clipConvertDataFromX11Worker(pClient, NULL, 0); + } + else /* Call with current data provided by X (default). */ + clipConvertDataFromX11Worker(pClient, pvSrc, (*pcLen) * (*piFormat) / 8); + } + + XtFree((char *)pvSrc); +} + +static int clipGetSelectionValue(PSHCLX11CTX pCtx, SHCLX11FMTIDX idxFmt, + CLIPREADX11CBREQ *pReq) +{ +#ifndef TESTCASE + XtGetSelectionValue(pCtx->pWidget, clipGetAtom(pCtx, "CLIPBOARD"), + clipAtomForX11Format(pCtx, idxFmt), + clipConvertDataFromX11, + reinterpret_cast(pReq), + CurrentTime); +#else + tstClipRequestData(pCtx, idxFmt, (void *)pReq); +#endif + + return VINF_SUCCESS; /** @todo Return real rc. */ +} + +/** + * Worker function for ShClX11ReadDataFromX11 which runs on the event thread. + * + * @param pvUserData Pointer to a CLIPREADX11CBREQ structure containing + * information about the clipboard read request. + * Must be free'd by the worker. + */ +static void ShClX11ReadDataFromX11Worker(void *pvUserData, void * /* interval */) +{ + AssertPtrReturnVoid(pvUserData); + + CLIPREADX11CBREQ *pReq = (CLIPREADX11CBREQ *)pvUserData; + SHCLX11CTX *pCtx = pReq->pCtx; + AssertPtrReturnVoid(pCtx); + + LogFlowFunc(("pReq->uFmtVBox=%#x, idxFmtX11=%#x\n", pReq->uFmtVBox, pReq->idxFmtX11)); + + int rc = VERR_NO_DATA; /* VBox thinks we have data and we don't. */ + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_XT_BUSY + const bool fXtBusy = clipGetXtBusy(pCtx); + clipSetXtBusy(pCtx, true); + if (fXtBusy) + { + /* If the clipboard is busy just fend off the request. */ + rc = VERR_TRY_AGAIN; + } + else +#endif + if (pReq->uFmtVBox & VBOX_SHCL_FMT_UNICODETEXT) + { + pReq->idxFmtX11 = pCtx->idxFmtText; + if (pReq->idxFmtX11 != SHCLX11FMT_INVALID) + { + /* Send out a request for the data to the current clipboard owner. */ + rc = clipGetSelectionValue(pCtx, pCtx->idxFmtText, pReq); + } + } + else if (pReq->uFmtVBox & VBOX_SHCL_FMT_BITMAP) + { + pReq->idxFmtX11 = pCtx->idxFmtBmp; + if (pReq->idxFmtX11 != SHCLX11FMT_INVALID) + { + /* Send out a request for the data to the current clipboard owner. */ + rc = clipGetSelectionValue(pCtx, pCtx->idxFmtBmp, pReq); + } + } + else if (pReq->uFmtVBox & VBOX_SHCL_FMT_HTML) + { + pReq->idxFmtX11 = pCtx->idxFmtHTML; + if (pReq->idxFmtX11 != SHCLX11FMT_INVALID) + { + /* Send out a request for the data to the current clipboard owner. */ + rc = clipGetSelectionValue(pCtx, pCtx->idxFmtHTML, pReq); + } + } +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + else if (pReq->uFmtVBox & VBOX_SHCL_FMT_URI_LIST) + { + pReq->idxFmtX11 = pCtx->idxFmtURI; + if (pReq->idxFmtX11 != SHCLX11FMT_INVALID) + { + /* Send out a request for the data to the current clipboard owner. */ + rc = clipGetSelectionValue(pCtx, pCtx->idxFmtURI, pReq); + } + } +#endif + else + { +#ifdef VBOX_WITH_SHARED_CLIPBOARD_XT_BUSY + clipSetXtBusy(pCtx, false); +#endif + rc = VERR_NOT_IMPLEMENTED; + } + + if (RT_FAILURE(rc)) + { + /* The clipboard callback was never scheduled, so we must signal + * that the request processing is finished and clean up ourselves. */ + SHCLX11READDATAREQ SendData; + RT_ZERO(SendData); + SendData.pReq = pReq->pReq; + SendData.rcCompletion = rc; + + pCtx->Callbacks.pfnOnSendDataToDest(pReq->pCtx->pFrontend, NULL /* pv */ ,0 /* cb */, &SendData); + RTMemFree(pReq); + } + + LogFlowFuncLeaveRC(rc); +} + +/** + * Called when VBox wants to read the X11 clipboard. + * + * @returns VBox status code. + * @retval VERR_NO_DATA if format is supported but no data is available currently. + * @retval VERR_NOT_IMPLEMENTED if the format is not implemented. + * @param pCtx Context data for the clipboard backend. + * @param uFmt The format that the VBox would like to receive the data in. + * @param pReq Read callback request to use. Will be free'd in the callback on success. + * Otherwise the caller has to free this again on error. + * + * @note We allocate a request structure which must be freed by the worker. + */ +int ShClX11ReadDataFromX11(PSHCLX11CTX pCtx, SHCLFORMAT uFmt, CLIPREADCBREQ *pReq) +{ + AssertPtrReturn(pReq, VERR_INVALID_POINTER); + /* + * Immediately return if we are not connected to the X server. + */ + if (!pCtx->fHaveX11) + return VERR_NO_DATA; + + int rc = VINF_SUCCESS; + + CLIPREADX11CBREQ *pX11Req = (CLIPREADX11CBREQ *)RTMemAllocZ(sizeof(CLIPREADX11CBREQ)); + if (pX11Req) + { + pX11Req->pCtx = pCtx; + pX11Req->uFmtVBox = uFmt; + pX11Req->pReq = pReq; + + /* We use this to schedule a worker function on the event thread. */ + rc = clipThreadScheduleCall(pCtx, ShClX11ReadDataFromX11Worker, (XtPointer)pX11Req); + if (RT_FAILURE(rc)) + RTMemFree(pX11Req); + } + else + rc = VERR_NO_MEMORY; + + LogFlowFuncLeaveRC(rc); + return rc; +} diff --git a/src/VBox/GuestHost/SharedClipboard/testcase/Makefile.kmk b/src/VBox/GuestHost/SharedClipboard/testcase/Makefile.kmk new file mode 100644 index 00000000..5b073ea1 --- /dev/null +++ b/src/VBox/GuestHost/SharedClipboard/testcase/Makefile.kmk @@ -0,0 +1,76 @@ +# $Id: Makefile.kmk $ +## @file +# Sub-Makefile for the Shared Clipboard Guest/Host testcases. +# + +# +# Copyright (C) 2011-2023 Oracle and/or its affiliates. +# +# This file is part of VirtualBox base platform packages, as +# available from https://www.virtualbox.org. +# +# 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, in version 3 of the +# License. +# +# 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, see . +# +# SPDX-License-Identifier: GPL-3.0-only +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + +if defined(VBOX_WITH_TESTCASES) && !defined(VBOX_ONLY_ADDITIONS) && !defined(VBOX_ONLY_SDK) + if1of ($(KBUILD_TARGET), freebsd linux netbsd openbsd solaris) + + PROGRAMS += tstClipboardGH-X11 tstClipboardGH-X11Smoke + TESTING += \ + $(tstClipboardGH-X11_0_OUTDIR)/tstClipboardGH-X11.run \ + $(tstClipboardGH-X11_0_OUTDIR)/tstClipboardGH-X11Smoke.run + + tstClipboardGH-X11_TEMPLATE = VBoxR3TstExe + tstClipboardGH-X11_DEFS = VBOX_WITH_HGCM UNIT_TEST TESTCASE + tstClipboardGH-X11_SOURCES = \ + tstClipboardGH-X11.cpp \ + ../clipboard-x11.cpp \ + ../clipboard-common.cpp + tstClipboardGH-X11_CXXFLAGS += -Wno-array-bounds + tstClipboardGH-X11_LIBS = X11 Xt + tstClipboardGH-X11_CLEAN = $(tstClipboardGH-X11_0_OUTDIR)/tstClipboardGH-X11.run + + tstClipboardGH-X11Smoke_TEMPLATE = VBoxR3TstExe + tstClipboardGH-X11Smoke_DEFS = VBOX_WITH_HGCM SMOKETEST + tstClipboardGH-X11Smoke_SOURCES = \ + tstClipboardGH-X11Smoke.cpp \ + ../clipboard-x11.cpp \ + ../clipboard-common.cpp + tstClipboardGH-X11Smoke_LIBPATH = $(VBOX_LIBPATH_X11) + tstClipboardGH-X11Smoke_LIBS = X11 Xt + tstClipboardGH-X11Smoke_CLEAN = $(tstClipboardGH-X11Smoke_0_OUTDIR)/tstClipboardGH-X11Smoke.run + + $$(tstClipboardGH-X11_0_OUTDIR)/tstClipboardGH-X11.run: $$(tstClipboardGH-X11_1_STAGE_TARGET) + export VBOX_LOG_DEST=nofile; $(tstClipboardGH-X11_1_STAGE_TARGET) quiet + $(QUIET)$(APPEND) -t "$@" "done" + + $$(tstClipboardGH-X11Smoke_0_OUTDIR)/tstClipboardGH-X11Smoke.run: $$(tstClipboardGH-X11Smoke_1_STAGE_TARGET) + export VBOX_LOG_DEST=nofile; $(tstClipboardGH-X11Smoke_1_STAGE_TARGET) quiet + $(QUIET)$(APPEND) -t "$@" "done" + + if defined(VBOX_WITH_QTGUI) + include $(PATH_SUB_CURRENT)/tstClipboardQt/Makefile.kmk + endif + + + endif +endif + +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/GuestHost/SharedClipboard/testcase/tstClipboardGH-X11.cpp b/src/VBox/GuestHost/SharedClipboard/testcase/tstClipboardGH-X11.cpp new file mode 100644 index 00000000..f9bc19c1 --- /dev/null +++ b/src/VBox/GuestHost/SharedClipboard/testcase/tstClipboardGH-X11.cpp @@ -0,0 +1,941 @@ +/* $Id: tstClipboardGH-X11.cpp $ */ +/** @file + * Shared Clipboard guest/host X11 code test cases. + */ + +/* + * Copyright (C) 2011-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + + +/********************************************************************************************************************************* +* Externals * +*********************************************************************************************************************************/ +extern SHCLX11FMTTABLE g_aFormats[]; + +extern void clipUpdateX11Targets(PSHCLX11CTX pCtx, SHCLX11FMTIDX *pTargets, size_t cTargets); +extern void clipReportEmpty(PSHCLX11CTX pCtx); +extern void clipConvertDataFromX11Worker(void *pClient, void *pvSrc, unsigned cbSrc); +extern SHCLX11FMTIDX clipGetTextFormatFromTargets(PSHCLX11CTX pCtx, SHCLX11FMTIDX *pTargets, size_t cTargets); +extern SHCLX11FMT clipRealFormatForX11Format(SHCLX11FMTIDX uFmtIdx); +extern Atom clipGetAtom(PSHCLX11CTX pCtx, const char *pcszName); +extern void clipQueryX11Targets(PSHCLX11CTX pCtx); +extern size_t clipReportMaxX11Formats(void); + + +/********************************************************************************************************************************* +* Internal prototypes * +*********************************************************************************************************************************/ +static SHCLX11FMTIDX tstClipFindX11FormatByAtomText(const char *pcszAtom); + + +/********************************************************************************************************************************* +* Prototypes, used for testcases by clipboard-x11.cpp * +*********************************************************************************************************************************/ +void tstRequestTargets(SHCLX11CTX* pCtx); +void tstClipRequestData(PSHCLX11CTX pCtx, SHCLX11FMTIDX target, void *closure); +void tstThreadScheduleCall(void (*proc)(void *, void *), void *client_data); + + +/********************************************************************************************************************************* +* Own callback implementations * +*********************************************************************************************************************************/ +extern DECLCALLBACK(void) clipQueryX11TargetsCallback(Widget widget, XtPointer pClient, + Atom * /* selection */, Atom *atomType, + XtPointer pValue, long unsigned int *pcLen, + int *piFormat); + + +/********************************************************************************************************************************* +* Defines * +*********************************************************************************************************************************/ +#define TESTCASE_WIDGET_ID (Widget)0xffff + + +/* For the purpose of the test case, we just execute the procedure to be + * scheduled, as we are running single threaded. */ +void tstThreadScheduleCall(void (*proc)(void *, void *), void *client_data) +{ + proc(client_data, NULL); +} + +/* The data in the simulated VBox clipboard. */ +static int g_tst_rcDataVBox = VINF_SUCCESS; +static void *g_tst_pvDataVBox = NULL; +static uint32_t g_tst_cbDataVBox = 0; + +/* Set empty data in the simulated VBox clipboard. */ +static void tstClipEmptyVBox(PSHCLX11CTX pCtx, int retval) +{ + g_tst_rcDataVBox = retval; + RTMemFree(g_tst_pvDataVBox); + g_tst_pvDataVBox = NULL; + g_tst_cbDataVBox = 0; + ShClX11ReportFormatsToX11(pCtx, 0); +} + +/* Set the data in the simulated VBox clipboard. */ +static int tstClipSetVBoxUtf16(PSHCLX11CTX pCtx, int retval, + const char *pcszData, size_t cb) +{ + PRTUTF16 pwszData = NULL; + size_t cwData = 0; + int rc = RTStrToUtf16Ex(pcszData, RTSTR_MAX, &pwszData, 0, &cwData); + if (RT_FAILURE(rc)) + return rc; + AssertReturn(cb <= cwData * 2 + 2, VERR_BUFFER_OVERFLOW); + void *pv = RTMemDup(pwszData, cb); + RTUtf16Free(pwszData); + if (pv == NULL) + return VERR_NO_MEMORY; + if (g_tst_pvDataVBox) + RTMemFree(g_tst_pvDataVBox); + g_tst_rcDataVBox = retval; + g_tst_pvDataVBox = pv; + g_tst_cbDataVBox = cb; + ShClX11ReportFormatsToX11(pCtx, VBOX_SHCL_FMT_UNICODETEXT); + return VINF_SUCCESS; +} + +Display *XtDisplay(Widget w) { NOREF(w); return (Display *) 0xffff; } + +void XtAppSetExitFlag(XtAppContext app_context) { NOREF(app_context); } + +void XtDestroyWidget(Widget w) { NOREF(w); } + +XtAppContext XtCreateApplicationContext(void) { return (XtAppContext)0xffff; } + +void XtDestroyApplicationContext(XtAppContext app_context) { NOREF(app_context); } + +void XtToolkitInitialize(void) {} + +Boolean XtToolkitThreadInitialize(void) { return True; } + +Display *XtOpenDisplay(XtAppContext app_context, + _Xconst _XtString display_string, + _Xconst _XtString application_name, + _Xconst _XtString application_class, + XrmOptionDescRec *options, Cardinal num_options, + int *argc, char **argv) +{ + RT_NOREF8(app_context, display_string, application_name, application_class, options, num_options, argc, argv); + return (Display *)0xffff; +} + +Widget XtVaAppCreateShell(_Xconst _XtString application_name, _Xconst _XtString application_class, + WidgetClass widget_class, Display *display, ...) +{ + RT_NOREF(application_name, application_class, widget_class, display); + return TESTCASE_WIDGET_ID; +} + +void XtSetMappedWhenManaged(Widget widget, _XtBoolean mapped_when_managed) { RT_NOREF(widget, mapped_when_managed); } + +void XtRealizeWidget(Widget widget) { NOREF(widget); } + +XtInputId XtAppAddInput(XtAppContext app_context, int source, XtPointer condition, XtInputCallbackProc proc, XtPointer closure) +{ + RT_NOREF(app_context, source, condition, proc, closure); + return 0xffff; +} + +/* Atoms we need other than the formats we support. */ +static const char *g_tst_apszSupAtoms[] = +{ + "PRIMARY", "CLIPBOARD", "TARGETS", "MULTIPLE", "TIMESTAMP" +}; + +/* This just looks for the atom names in a couple of tables and returns an + * index with an offset added. */ +Atom XInternAtom(Display *, const char *pcsz, int) +{ + Atom atom = 0; + size_t const cFormats = clipReportMaxX11Formats(); + size_t i; + for (i = 0; i < cFormats; ++i) + { + if (!strcmp(pcsz, g_aFormats[i].pcszAtom)) + atom = (Atom) (i + 0x1000); + } + for (i = 0; i < RT_ELEMENTS(g_tst_apszSupAtoms); ++i) + if (!strcmp(pcsz, g_tst_apszSupAtoms[i])) + atom = (Atom) (i + 0x2000); + Assert(atom); /* Have we missed any atoms? */ + return atom; +} + +/* Take a request for the targets we are currently offering. */ +static SHCLX11FMTIDX g_tst_aSelTargetsIdx[10] = { 0 }; +static size_t g_tst_cTargets = 0; + +void tstRequestTargets(SHCLX11CTX* pCtx) +{ + clipUpdateX11Targets(pCtx, g_tst_aSelTargetsIdx, g_tst_cTargets); +} + +/* The current values of the X selection, which will be returned to the + * XtGetSelectionValue callback. */ +static Atom g_tst_atmSelType = 0; +static const void *g_tst_pSelData = NULL; +static unsigned long g_tst_cSelData = 0; +static int g_tst_selFormat = 0; + +void tstClipRequestData(PSHCLX11CTX pCtx, SHCLX11FMTIDX target, void *closure) +{ + RT_NOREF(pCtx); + unsigned long count = 0; + int format = 0; + if (target != g_tst_aSelTargetsIdx[0]) + { + clipConvertDataFromX11Worker(closure, NULL, 0); /* Could not convert to target. */ + return; + } + void *pValue = NULL; + pValue = g_tst_pSelData ? RTMemDup(g_tst_pSelData, g_tst_cSelData) : NULL; + count = g_tst_pSelData ? g_tst_cSelData : 0; + format = g_tst_selFormat; + if (!pValue) + { + count = 0; + format = 0; + } + clipConvertDataFromX11Worker(closure, pValue, count * format / 8); + if (pValue) + RTMemFree(pValue); +} + +/* The formats currently on offer from X11 via the shared clipboard. */ +static uint32_t g_tst_uX11Formats = 0; + +static uint32_t tstClipQueryFormats(void) +{ + return g_tst_uX11Formats; +} + +static void tstClipInvalidateFormats(void) +{ + g_tst_uX11Formats = ~0; +} + +/* Does our clipboard code currently own the selection? */ +static bool g_tst_fOwnsSel = false; +/* The procedure that is called when we should convert the selection to a + * given format. */ +static XtConvertSelectionProc g_tst_pfnSelConvert = NULL; +/* The procedure which is called when we lose the selection. */ +static XtLoseSelectionProc g_tst_pfnSelLose = NULL; +/* The procedure which is called when the selection transfer has completed. */ +static XtSelectionDoneProc g_tst_pfnSelDone = NULL; + +Boolean XtOwnSelection(Widget widget, Atom selection, Time time, + XtConvertSelectionProc convert, + XtLoseSelectionProc lose, + XtSelectionDoneProc done) +{ + RT_NOREF(widget, time); + if (selection != XInternAtom(NULL, "CLIPBOARD", 0)) + return True; /* We don't really care about this. */ + g_tst_fOwnsSel = true; /* Always succeed. */ + g_tst_pfnSelConvert = convert; + g_tst_pfnSelLose = lose; + g_tst_pfnSelDone = done; + return True; +} + +void XtDisownSelection(Widget widget, Atom selection, Time time) +{ + RT_NOREF(widget, time, selection); + g_tst_fOwnsSel = false; + g_tst_pfnSelConvert = NULL; + g_tst_pfnSelLose = NULL; + g_tst_pfnSelDone = NULL; +} + +/* Request the shared clipboard to convert its data to a given format. */ +static bool tstClipConvertSelection(const char *pcszTarget, Atom *type, + XtPointer *value, unsigned long *length, + int *format) +{ + Atom target = XInternAtom(NULL, pcszTarget, 0); + if (target == 0) + return false; + /* Initialise all return values in case we make a quick exit. */ + *type = XA_STRING; + *value = NULL; + *length = 0; + *format = 0; + if (!g_tst_fOwnsSel) + return false; + if (!g_tst_pfnSelConvert) + return false; + Atom clipAtom = XInternAtom(NULL, "CLIPBOARD", 0); + if (!g_tst_pfnSelConvert(TESTCASE_WIDGET_ID, &clipAtom, &target, type, + value, length, format)) + return false; + if (g_tst_pfnSelDone) + g_tst_pfnSelDone(TESTCASE_WIDGET_ID, &clipAtom, &target); + return true; +} + +/* Set the current X selection data */ +static void tstClipSetSelectionValues(const char *pcszTarget, Atom type, + const void *data, + unsigned long count, int format) +{ + Atom clipAtom = XInternAtom(NULL, "CLIPBOARD", 0); + g_tst_aSelTargetsIdx[0] = tstClipFindX11FormatByAtomText(pcszTarget); + g_tst_cTargets = 1; + g_tst_atmSelType = type; + g_tst_pSelData = data; + g_tst_cSelData = count; + g_tst_selFormat = format; + if (g_tst_pfnSelLose) + g_tst_pfnSelLose(TESTCASE_WIDGET_ID, &clipAtom); + g_tst_fOwnsSel = false; +} + +static void tstClipSendTargetUpdate(PSHCLX11CTX pCtx) +{ + clipQueryX11Targets(pCtx); +} + +/* Configure if and how the X11 TARGETS clipboard target will fail. */ +static void tstClipSetTargetsFailure(void) +{ + g_tst_cTargets = 0; +} + +char *XtMalloc(Cardinal size) +{ + return (char *) RTMemAlloc(size); +} + +void XtFree(char *ptr) +{ + RTMemFree((void *)ptr); +} + +char *XGetAtomName(Display *display, Atom atom) +{ + RT_NOREF(display); + const char *pcszName = NULL; + if (atom < 0x1000) + return NULL; + if (0x1000 <= atom && atom < 0x2000) + { + unsigned index = atom - 0x1000; + AssertReturn(index < clipReportMaxX11Formats(), NULL); + pcszName = g_aFormats[index].pcszAtom; + } + else + { + unsigned index = atom - 0x2000; + AssertReturn(index < RT_ELEMENTS(g_tst_apszSupAtoms), NULL); + pcszName = g_tst_apszSupAtoms[index]; + } + return (char *)RTMemDup(pcszName, sizeof(pcszName) + 1); +} + +int XFree(void *data) +{ + RTMemFree(data); + return 0; +} + +void XFreeStringList(char **list) +{ + if (list) + RTMemFree(*list); + RTMemFree(list); +} + +#define TESTCASE_MAX_BUF_SIZE 256 + +static int g_tst_rcCompleted = VINF_SUCCESS; +static int g_tst_cbCompleted = 0; +static CLIPREADCBREQ *g_tst_pCompletedReq = NULL; +static char g_tst_abCompletedBuf[TESTCASE_MAX_BUF_SIZE]; + +static DECLCALLBACK(int) tstShClReportFormatsCallback(PSHCLCONTEXT pCtx, uint32_t fFormats, void *pvUser) +{ + RT_NOREF(pCtx, pvUser); + g_tst_uX11Formats = fFormats; + return VINF_SUCCESS; +} + +static DECLCALLBACK(int) tstShClOnRequestDataFromSourceCallback(PSHCLCONTEXT pCtx, SHCLFORMAT uFmt, void **ppv, uint32_t *pcb, void *pvUser) +{ + RT_NOREF(pCtx, uFmt, pvUser); + *pcb = g_tst_cbDataVBox; + if (g_tst_pvDataVBox != NULL) + { + void *pv = RTMemDup(g_tst_pvDataVBox, g_tst_cbDataVBox); + *ppv = pv; + return pv != NULL ? g_tst_rcDataVBox : VERR_NO_MEMORY; + } + *ppv = NULL; + return g_tst_rcDataVBox; +} + +static DECLCALLBACK(int) tstShClOnSendDataToDestCallback(PSHCLCONTEXT pCtx, void *pv, uint32_t cb, void *pvUser) +{ + RT_NOREF(pCtx); + + PSHCLX11READDATAREQ pData = (PSHCLX11READDATAREQ)pvUser; + + if (cb <= TESTCASE_MAX_BUF_SIZE) + { + g_tst_rcCompleted = pData->rcCompletion; + if (cb != 0) + memcpy(g_tst_abCompletedBuf, pv, cb); + } + else + g_tst_rcCompleted = VERR_BUFFER_OVERFLOW; + g_tst_cbCompleted = cb; + g_tst_pCompletedReq = pData->pReq; + + return VINF_SUCCESS; +} + +/** + * Looks up the X11 format matching a given X11 atom text. + * + * @returns the format on success, NIL_CLIPX11FORMAT on failure + * @param pcszAtom Atom text to look up format for. + */ +static SHCLX11FMTIDX tstClipFindX11FormatByAtomText(const char *pcszAtom) +{ + const size_t j = clipReportMaxX11Formats(); + + for (unsigned i = 0; i < j; ++i) + { + if (!strcmp(g_aFormats[i].pcszAtom, pcszAtom)) + return i; + } + return NIL_CLIPX11FORMAT; +} + +static bool tstClipTextFormatConversion(PSHCLX11CTX pCtx) +{ + bool fSuccess = true; + SHCLX11FMTIDX targets[2]; + SHCLX11FMTIDX x11Format; + targets[0] = tstClipFindX11FormatByAtomText("text/plain"); + targets[1] = tstClipFindX11FormatByAtomText("image/bmp"); + x11Format = clipGetTextFormatFromTargets(pCtx, targets, 2); + if (clipRealFormatForX11Format(x11Format) != SHCLX11FMT_TEXT) + fSuccess = false; + targets[0] = tstClipFindX11FormatByAtomText("UTF8_STRING"); + targets[1] = tstClipFindX11FormatByAtomText("text/plain"); + x11Format = clipGetTextFormatFromTargets(pCtx, targets, 2); + if (clipRealFormatForX11Format(x11Format) != SHCLX11FMT_UTF8) + fSuccess = false; + return fSuccess; +} + +static void tstClipGetCompletedRequest(int *prc, char ** ppc, uint32_t *pcb, CLIPREADCBREQ **ppReq) +{ + *prc = g_tst_rcCompleted; + *ppc = g_tst_abCompletedBuf; + *pcb = g_tst_cbCompleted; + *ppReq = g_tst_pCompletedReq; +} + +static void tstStringFromX11(RTTEST hTest, PSHCLX11CTX pCtx, + const char *pcszExp, int rcExp) +{ + bool retval = true; + tstClipSendTargetUpdate(pCtx); + if (tstClipQueryFormats() != VBOX_SHCL_FMT_UNICODETEXT) + { + RTTestFailed(hTest, "Wrong targets reported: %02X\n", tstClipQueryFormats()); + } + else + { + char *pc; + CLIPREADCBREQ *pReq = (CLIPREADCBREQ *)&pReq, *pReqRet = NULL; + ShClX11ReadDataFromX11(pCtx, VBOX_SHCL_FMT_UNICODETEXT, pReq); + int rc = VINF_SUCCESS; + uint32_t cbActual = 0; + tstClipGetCompletedRequest(&rc, &pc, &cbActual, &pReqRet); + if (rc != rcExp) + RTTestFailed(hTest, "Wrong return code, expected %Rrc, got %Rrc\n", + rcExp, rc); + else if (pReqRet != pReq) + RTTestFailed(hTest, "Wrong returned request data, expected %p, got %p\n", + pReq, pReqRet); + else if (RT_FAILURE(rcExp)) + retval = true; + else + { + RTUTF16 wcExp[TESTCASE_MAX_BUF_SIZE / 2]; + RTUTF16 *pwcExp = wcExp; + size_t cwc = 0; + rc = RTStrToUtf16Ex(pcszExp, RTSTR_MAX, &pwcExp, + RT_ELEMENTS(wcExp), &cwc); + size_t cbExp = cwc * 2 + 2; + AssertRC(rc); + if (RT_SUCCESS(rc)) + { + if (cbActual != cbExp) + { + RTTestFailed(hTest, "Returned string is the wrong size, string \"%.*ls\", size %u, expected \"%s\", size %u\n", + RT_MIN(TESTCASE_MAX_BUF_SIZE, cbActual), pc, cbActual, + pcszExp, cbExp); + } + else + { + if (memcmp(pc, wcExp, cbExp) == 0) + retval = true; + else + RTTestFailed(hTest, "Returned string \"%.*ls\" does not match expected string \"%s\"\n", + TESTCASE_MAX_BUF_SIZE, pc, pcszExp); + } + } + } + } + if (!retval) + RTTestFailureDetails(hTest, "Expected: string \"%s\", rc %Rrc\n", + pcszExp, rcExp); +} + +static void tstLatin1FromX11(RTTEST hTest, PSHCLX11CTX pCtx, + const char *pcszExp, int rcExp) +{ + bool retval = false; + tstClipSendTargetUpdate(pCtx); + if (tstClipQueryFormats() != VBOX_SHCL_FMT_UNICODETEXT) + RTTestFailed(hTest, "Wrong targets reported: %02X\n", + tstClipQueryFormats()); + else + { + char *pc; + CLIPREADCBREQ *pReq = (CLIPREADCBREQ *)&pReq, *pReqRet = NULL; + ShClX11ReadDataFromX11(pCtx, VBOX_SHCL_FMT_UNICODETEXT, pReq); + int rc = VINF_SUCCESS; + uint32_t cbActual = 0; + tstClipGetCompletedRequest(&rc, &pc, &cbActual, &pReqRet); + if (rc != rcExp) + RTTestFailed(hTest, "Wrong return code, expected %Rrc, got %Rrc\n", + rcExp, rc); + else if (pReqRet != pReq) + RTTestFailed(hTest, "Wrong returned request data, expected %p, got %p\n", + pReq, pReqRet); + else if (RT_FAILURE(rcExp)) + retval = true; + else + { + RTUTF16 wcExp[TESTCASE_MAX_BUF_SIZE / 2]; + //RTUTF16 *pwcExp = wcExp; - unused + size_t cwc; + for (cwc = 0; cwc == 0 || pcszExp[cwc - 1] != '\0'; ++cwc) + wcExp[cwc] = pcszExp[cwc]; + size_t cbExp = cwc * 2; + if (cbActual != cbExp) + { + RTTestFailed(hTest, "Returned string is the wrong size, string \"%.*ls\", size %u, expected \"%s\", size %u\n", + RT_MIN(TESTCASE_MAX_BUF_SIZE, cbActual), pc, cbActual, + pcszExp, cbExp); + } + else + { + if (memcmp(pc, wcExp, cbExp) == 0) + retval = true; + else + RTTestFailed(hTest, "Returned string \"%.*ls\" does not match expected string \"%s\"\n", + TESTCASE_MAX_BUF_SIZE, pc, pcszExp); + } + } + } + if (!retval) + RTTestFailureDetails(hTest, "Expected: string \"%s\", rc %Rrc\n", + pcszExp, rcExp); +} + +static void tstStringFromVBox(RTTEST hTest, PSHCLX11CTX pCtx, const char *pcszTarget, Atom typeExp, const char *valueExp) +{ + RT_NOREF(pCtx); + bool retval = false; + Atom type; + XtPointer value = NULL; + unsigned long length; + int format; + size_t lenExp = strlen(valueExp); + if (tstClipConvertSelection(pcszTarget, &type, &value, &length, &format)) + { + if ( type != typeExp + || length != lenExp + || format != 8 + || memcmp((const void *) value, (const void *)valueExp, + lenExp)) + { + RTTestFailed(hTest, "Bad data: type %d, (expected %d), length %u, (expected %u), format %d (expected %d), value \"%.*s\" (expected \"%.*s\")\n", + type, typeExp, length, lenExp, format, 8, + RT_MIN(length, 20), value, RT_MIN(lenExp, 20), valueExp); + } + else + retval = true; + } + else + RTTestFailed(hTest, "Conversion failed\n"); + XtFree((char *)value); + if (!retval) + RTTestFailureDetails(hTest, "Conversion to %s, expected \"%s\"\n", + pcszTarget, valueExp); +} + +static void tstNoX11(PSHCLX11CTX pCtx, const char *pcszTestCtx) +{ + CLIPREADCBREQ *pReq = (CLIPREADCBREQ *)&pReq; + int rc = ShClX11ReadDataFromX11(pCtx, VBOX_SHCL_FMT_UNICODETEXT, pReq); + RTTESTI_CHECK_MSG(rc == VERR_NO_DATA, ("context: %s\n", pcszTestCtx)); +} + +static void tstStringFromVBoxFailed(RTTEST hTest, PSHCLX11CTX pCtx, const char *pcszTarget) +{ + RT_NOREF(pCtx); + Atom type; + XtPointer value = NULL; + unsigned long length; + int format; + RTTEST_CHECK_MSG(hTest, !tstClipConvertSelection(pcszTarget, &type, &value, + &length, &format), + (hTest, "Conversion to target %s, should have failed but didn't, returned type %d, length %u, format %d, value \"%.*s\"\n", + pcszTarget, type, length, format, RT_MIN(length, 20), + value)); + XtFree((char *)value); +} + +static void tstNoSelectionOwnership(PSHCLX11CTX pCtx, const char *pcszTestCtx) +{ + RT_NOREF(pCtx); + RTTESTI_CHECK_MSG(!g_tst_fOwnsSel, ("context: %s\n", pcszTestCtx)); +} + +static void tstBadFormatRequestFromHost(RTTEST hTest, PSHCLX11CTX pCtx) +{ + tstClipSetSelectionValues("UTF8_STRING", XA_STRING, "hello world", + sizeof("hello world"), 8); + tstClipSendTargetUpdate(pCtx); + if (tstClipQueryFormats() != VBOX_SHCL_FMT_UNICODETEXT) + RTTestFailed(hTest, "Wrong targets reported: %02X\n", + tstClipQueryFormats()); + else + { + char *pc; + CLIPREADCBREQ *pReq = (CLIPREADCBREQ *)&pReq, *pReqRet = NULL; + ShClX11ReadDataFromX11(pCtx, 0xF000 /* vboxFormat */, pReq); /* Bad format. */ + int rc = VINF_SUCCESS; + uint32_t cbActual = 0; + tstClipGetCompletedRequest(&rc, &pc, &cbActual, &pReqRet); + if (rc != VERR_NOT_IMPLEMENTED) + RTTestFailed(hTest, "Wrong return code, expected VERR_NOT_IMPLEMENTED, got %Rrc\n", + rc); + tstClipSetSelectionValues("", XA_STRING, "", sizeof(""), 8); + tstClipSendTargetUpdate(pCtx); + if (tstClipQueryFormats() == VBOX_SHCL_FMT_UNICODETEXT) + RTTestFailed(hTest, "Failed to report targets after bad host request.\n"); + } +} + +int main() +{ + /* + * Init the runtime, test and say hello. + */ + RTTEST hTest; + int rc = RTTestInitAndCreate("tstClipboardGH-X11", &hTest); + if (RT_FAILURE(rc)) + return RTEXITCODE_FAILURE; + RTTestBanner(hTest); + + /* + * Run the tests. + */ + SHCLCALLBACKS Callbacks; + RT_ZERO(Callbacks); + Callbacks.pfnReportFormats = tstShClReportFormatsCallback; + Callbacks.pfnOnRequestDataFromSource = tstShClOnRequestDataFromSourceCallback; + Callbacks.pfnOnSendDataToDest = tstShClOnSendDataToDestCallback; + + SHCLX11CTX X11Ctx; + rc = ShClX11Init(&X11Ctx, &Callbacks, NULL /* pParent */, false /* fHeadless */); + AssertRCReturn(rc, RTEXITCODE_FAILURE); + + char *pc; + uint32_t cbActual; + CLIPREADCBREQ *pReq = (CLIPREADCBREQ *)&pReq, *pReqRet = NULL; + + /* UTF-8 from X11 */ + RTTestSub(hTest, "reading UTF-8 from X11"); + /* Simple test */ + tstClipSetSelectionValues("UTF8_STRING", XA_STRING, "hello world", + sizeof("hello world"), 8); + tstStringFromX11(hTest, &X11Ctx, "hello world", VINF_SUCCESS); + /* With an embedded carriage return */ + tstClipSetSelectionValues("text/plain;charset=UTF-8", XA_STRING, + "hello\nworld", sizeof("hello\nworld"), 8); + tstStringFromX11(hTest, &X11Ctx, "hello\r\nworld", VINF_SUCCESS); + /* With an embedded CRLF */ + tstClipSetSelectionValues("text/plain;charset=UTF-8", XA_STRING, + "hello\r\nworld", sizeof("hello\r\nworld"), 8); + tstStringFromX11(hTest, &X11Ctx, "hello\r\r\nworld", VINF_SUCCESS); + /* With an embedded LFCR */ + tstClipSetSelectionValues("text/plain;charset=UTF-8", XA_STRING, + "hello\n\rworld", sizeof("hello\n\rworld"), 8); + tstStringFromX11(hTest, &X11Ctx, "hello\r\n\rworld", VINF_SUCCESS); + /* An empty string */ + tstClipSetSelectionValues("text/plain;charset=utf-8", XA_STRING, "", + sizeof(""), 8); + tstStringFromX11(hTest, &X11Ctx, "", VINF_SUCCESS); + /* With an embedded UTF-8 character. */ + tstClipSetSelectionValues("STRING", XA_STRING, + "100\xE2\x82\xAC" /* 100 Euro */, + sizeof("100\xE2\x82\xAC"), 8); + tstStringFromX11(hTest, &X11Ctx, "100\xE2\x82\xAC", VINF_SUCCESS); + /* A non-zero-terminated string */ + tstClipSetSelectionValues("TEXT", XA_STRING, + "hello world", sizeof("hello world") - 1, 8); + tstStringFromX11(hTest, &X11Ctx, "hello world", VINF_SUCCESS); + + /* Latin1 from X11 */ + RTTestSub(hTest, "reading Latin1 from X11"); + /* Simple test */ + tstClipSetSelectionValues("STRING", XA_STRING, "Georges Dupr\xEA", + sizeof("Georges Dupr\xEA"), 8); + tstLatin1FromX11(hTest, &X11Ctx, "Georges Dupr\xEA", VINF_SUCCESS); + /* With an embedded carriage return */ + tstClipSetSelectionValues("TEXT", XA_STRING, "Georges\nDupr\xEA", + sizeof("Georges\nDupr\xEA"), 8); + tstLatin1FromX11(hTest, &X11Ctx, "Georges\r\nDupr\xEA", VINF_SUCCESS); + /* With an embedded CRLF */ + tstClipSetSelectionValues("TEXT", XA_STRING, "Georges\r\nDupr\xEA", + sizeof("Georges\r\nDupr\xEA"), 8); + tstLatin1FromX11(hTest, &X11Ctx, "Georges\r\r\nDupr\xEA", VINF_SUCCESS); + /* With an embedded LFCR */ + tstClipSetSelectionValues("TEXT", XA_STRING, "Georges\n\rDupr\xEA", + sizeof("Georges\n\rDupr\xEA"), 8); + tstLatin1FromX11(hTest, &X11Ctx, "Georges\r\n\rDupr\xEA", VINF_SUCCESS); + /* A non-zero-terminated string */ + tstClipSetSelectionValues("text/plain", XA_STRING, + "Georges Dupr\xEA!", + sizeof("Georges Dupr\xEA!") - 1, 8); + tstLatin1FromX11(hTest, &X11Ctx, "Georges Dupr\xEA!", VINF_SUCCESS); + + /* + * Unknown X11 format + */ + RTTestSub(hTest, "handling of an unknown X11 format"); + tstClipInvalidateFormats(); + tstClipSetSelectionValues("CLIPBOARD", XA_STRING, "Test", + sizeof("Test"), 8); + tstClipSendTargetUpdate(&X11Ctx); + RTTEST_CHECK_MSG(hTest, tstClipQueryFormats() == 0, + (hTest, "Failed to send a format update notification\n")); + + /* + * Timeout from X11 + */ + RTTestSub(hTest, "X11 timeout"); + tstClipSetSelectionValues("UTF8_STRING", XT_CONVERT_FAIL, NULL,0, 8); + tstStringFromX11(hTest, &X11Ctx, "", VERR_NO_DATA); + + /* + * No data in X11 clipboard + */ + RTTestSub(hTest, "a data request from an empty X11 clipboard"); + tstClipSetSelectionValues("UTF8_STRING", XA_STRING, NULL, + 0, 8); + ShClX11ReadDataFromX11(&X11Ctx, VBOX_SHCL_FMT_UNICODETEXT, pReq); + tstClipGetCompletedRequest(&rc, &pc, &cbActual, &pReqRet); + RTTEST_CHECK_MSG(hTest, rc == VERR_NO_DATA, + (hTest, "Returned %Rrc instead of VERR_NO_DATA\n", + rc)); + RTTEST_CHECK_MSG(hTest, pReqRet == pReq, + (hTest, "Wrong returned request data, expected %p, got %p\n", + pReq, pReqRet)); + + /* + * Ensure that VBox is notified when we return the CB to X11 + */ + RTTestSub(hTest, "notification of switch to X11 clipboard"); + tstClipInvalidateFormats(); + clipReportEmpty(&X11Ctx); + RTTEST_CHECK_MSG(hTest, tstClipQueryFormats() == 0, + (hTest, "Failed to send a format update (release) notification\n")); + + /* + * Request for an invalid VBox format from X11 + */ + RTTestSub(hTest, "a request for an invalid VBox format from X11"); + /* Testing for 0xffff will go into handling VBOX_SHCL_FMT_UNICODETEXT, where we don't have + * have any data at the moment so far, so this will return VERR_NO_DATA. */ + ShClX11ReadDataFromX11(&X11Ctx, 0xffff /* vboxFormat */, pReq); + tstClipGetCompletedRequest(&rc, &pc, &cbActual, &pReqRet); + RTTEST_CHECK_MSG(hTest, rc == VERR_NO_DATA, + (hTest, "Returned %Rrc instead of VERR_NO_DATA\n", + rc)); + RTTEST_CHECK_MSG(hTest, pReqRet == pReq, + (hTest, "Wrong returned request data, expected %p, got %p\n", + pReq, pReqRet)); + + /* + * Targets failure from X11 + */ + RTTestSub(hTest, "X11 targets conversion failure"); + tstClipSetSelectionValues("UTF8_STRING", XA_STRING, "hello world", + sizeof("hello world"), 8); + tstClipSetTargetsFailure(); + Atom atom = XA_STRING; + long unsigned int cLen = 0; + int format = 8; + clipQueryX11TargetsCallback(NULL, (XtPointer) &X11Ctx, NULL, &atom, NULL, &cLen, + &format); + RTTEST_CHECK_MSG(hTest, tstClipQueryFormats() == 0, + (hTest, "Wrong targets reported: %02X\n", + tstClipQueryFormats())); + + /* + * X11 text format conversion + */ + RTTestSub(hTest, "handling of X11 selection targets"); + RTTEST_CHECK_MSG(hTest, tstClipTextFormatConversion(&X11Ctx), + (hTest, "failed to select the right X11 text formats\n")); + /* + * UTF-8 from VBox + */ + RTTestSub(hTest, "reading UTF-8 from VBox"); + /* Simple test */ + tstClipSetVBoxUtf16(&X11Ctx, VINF_SUCCESS, "hello world", + sizeof("hello world") * 2); + tstStringFromVBox(hTest, &X11Ctx, "UTF8_STRING", + clipGetAtom(&X11Ctx, "UTF8_STRING"), "hello world"); + /* With an embedded carriage return */ + tstClipSetVBoxUtf16(&X11Ctx, VINF_SUCCESS, "hello\r\nworld", + sizeof("hello\r\nworld") * 2); + tstStringFromVBox(hTest, &X11Ctx, "text/plain;charset=UTF-8", + clipGetAtom(&X11Ctx, "text/plain;charset=UTF-8"), + "hello\nworld"); + /* With an embedded CRCRLF */ + tstClipSetVBoxUtf16(&X11Ctx, VINF_SUCCESS, "hello\r\r\nworld", + sizeof("hello\r\r\nworld") * 2); + tstStringFromVBox(hTest, &X11Ctx, "text/plain;charset=UTF-8", + clipGetAtom(&X11Ctx, "text/plain;charset=UTF-8"), + "hello\r\nworld"); + /* With an embedded CRLFCR */ + tstClipSetVBoxUtf16(&X11Ctx, VINF_SUCCESS, "hello\r\n\rworld", + sizeof("hello\r\n\rworld") * 2); + tstStringFromVBox(hTest, &X11Ctx, "text/plain;charset=UTF-8", + clipGetAtom(&X11Ctx, "text/plain;charset=UTF-8"), + "hello\n\rworld"); + /* An empty string */ + tstClipSetVBoxUtf16(&X11Ctx, VINF_SUCCESS, "", 2); + tstStringFromVBox(hTest, &X11Ctx, "text/plain;charset=utf-8", + clipGetAtom(&X11Ctx, "text/plain;charset=utf-8"), ""); + /* With an embedded UTF-8 character. */ + tstClipSetVBoxUtf16(&X11Ctx, VINF_SUCCESS, "100\xE2\x82\xAC" /* 100 Euro */, + 10); + tstStringFromVBox(hTest, &X11Ctx, "STRING", + clipGetAtom(&X11Ctx, "STRING"), "100\xE2\x82\xAC"); + /* A non-zero-terminated string */ + tstClipSetVBoxUtf16(&X11Ctx, VINF_SUCCESS, "hello world", + sizeof("hello world") * 2 - 2); + tstStringFromVBox(hTest, &X11Ctx, "TEXT", clipGetAtom(&X11Ctx, "TEXT"), + "hello world"); + + /* + * Timeout from VBox + */ + RTTestSub(hTest, "reading from VBox with timeout"); + tstClipEmptyVBox(&X11Ctx, VERR_TIMEOUT); + tstStringFromVBoxFailed(hTest, &X11Ctx, "UTF8_STRING"); + + /* + * No data in VBox clipboard + */ + RTTestSub(hTest, "an empty VBox clipboard"); + tstClipSetSelectionValues("TEXT", XA_STRING, "", sizeof(""), 8); + tstClipEmptyVBox(&X11Ctx, VINF_SUCCESS); + RTTEST_CHECK_MSG(hTest, g_tst_fOwnsSel, + (hTest, "VBox grabbed the clipboard with no data and we ignored it\n")); + tstStringFromVBoxFailed(hTest, &X11Ctx, "UTF8_STRING"); + + /* + * An unknown VBox format + */ + RTTestSub(hTest, "reading an unknown VBox format"); + tstClipSetSelectionValues("TEXT", XA_STRING, "", sizeof(""), 8); + tstClipSetVBoxUtf16(&X11Ctx, VINF_SUCCESS, "", 2); + ShClX11ReportFormatsToX11(&X11Ctx, 0xa0000); + RTTEST_CHECK_MSG(hTest, g_tst_fOwnsSel, + (hTest, "VBox grabbed the clipboard with unknown data and we ignored it\n")); + tstStringFromVBoxFailed(hTest, &X11Ctx, "UTF8_STRING"); + + /* + * VBox requests a bad format + */ + RTTestSub(hTest, "recovery from a bad format request"); + tstBadFormatRequestFromHost(hTest, &X11Ctx); + + ShClX11Destroy(&X11Ctx); + + /* + * Headless clipboard tests + */ + rc = ShClX11Init(&X11Ctx, &Callbacks, NULL /* pParent */, true /* fHeadless */); + AssertRCReturn(rc, RTEXITCODE_FAILURE); + + /* Read from X11 */ + RTTestSub(hTest, "reading from X11, headless clipboard"); + /* Simple test */ + tstClipSetVBoxUtf16(&X11Ctx, VINF_SUCCESS, "", + sizeof("") * 2); + tstClipSetSelectionValues("UTF8_STRING", XA_STRING, "hello world", + sizeof("hello world"), 8); + tstNoX11(&X11Ctx, "reading from X11, headless clipboard"); + + /* Read from VBox */ + RTTestSub(hTest, "reading from VBox, headless clipboard"); + /* Simple test */ + tstClipEmptyVBox(&X11Ctx, VERR_WRONG_ORDER); + tstClipSetSelectionValues("TEXT", XA_STRING, "", sizeof(""), 8); + tstClipSetVBoxUtf16(&X11Ctx, VINF_SUCCESS, "hello world", + sizeof("hello world") * 2); + tstNoSelectionOwnership(&X11Ctx, "reading from VBox, headless clipboard"); + + ShClX11Destroy(&X11Ctx); + + return RTTestSummaryAndDestroy(hTest); +} + diff --git a/src/VBox/GuestHost/SharedClipboard/testcase/tstClipboardGH-X11Smoke.cpp b/src/VBox/GuestHost/SharedClipboard/testcase/tstClipboardGH-X11Smoke.cpp new file mode 100644 index 00000000..226c6fac --- /dev/null +++ b/src/VBox/GuestHost/SharedClipboard/testcase/tstClipboardGH-X11Smoke.cpp @@ -0,0 +1,103 @@ +/* $Id: tstClipboardGH-X11Smoke.cpp $ */ +/** @file + * Shared Clipboard guest/host X11 code smoke tests. + */ + +/* + * Copyright (C) 2011-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +/* This is a simple test case that just starts a copy of the X11 clipboard + * backend, checks the X11 clipboard and exits. If ever needed I will add an + * interactive mode in which the user can read and copy to the clipboard from + * the command line. */ + +#include +#include +#include +#include + +#include +#include +#include + + +static DECLCALLBACK(int) tstShClReportFormatsCallback(PSHCLCONTEXT pCtx, uint32_t fFormats, void *pvUser) +{ + RT_NOREF(pCtx, fFormats, pvUser); + return VINF_SUCCESS; +} + +static DECLCALLBACK(int) tstShClOnRequestDataFromSourceCallback(PSHCLCONTEXT pCtx, SHCLFORMAT uFmt, void **ppv, uint32_t *pcb, void *pvUser) +{ + RT_NOREF(pCtx, uFmt, ppv, pcb, pvUser); + return VERR_NO_DATA; +} + +static DECLCALLBACK(int) tstShClOnSendDataToDest(PSHCLCONTEXT pCtx, void *pv, uint32_t cb, void *pvUser) +{ + RT_NOREF(pCtx, pv, cb, pvUser); + return VINF_SUCCESS; +} + +int main() +{ + /* + * Init the runtime, test and say hello. + */ + RTTEST hTest; + int rc = RTTestInitAndCreate("tstClipboardGH-X11Smoke", &hTest); + if (rc) + return rc; + RTTestBanner(hTest); + + /* + * Run the test. + */ + rc = VINF_SUCCESS; + /* We can't test anything without an X session, so just return success + * in that case. */ + if (!RTEnvExist("DISPLAY")) + { + RTTestPrintf(hTest, RTTESTLVL_INFO, + "X11 not available, not running test\n"); + return RTTestSummaryAndDestroy(hTest); + } + + SHCLCALLBACKS Callbacks; + RT_ZERO(Callbacks); + Callbacks.pfnReportFormats = tstShClReportFormatsCallback; + Callbacks.pfnOnRequestDataFromSource = tstShClOnRequestDataFromSourceCallback; + Callbacks.pfnOnSendDataToDest = tstShClOnSendDataToDest; + + SHCLX11CTX X11Ctx; + rc = ShClX11Init(&X11Ctx, &Callbacks, NULL /* pParent */, false); + AssertRCReturn(rc, 1); + rc = ShClX11ThreadStart(&X11Ctx, false /* fGrab */); + AssertRCReturn(rc, 1); + /* Give the clipboard time to synchronise. */ + RTThreadSleep(500); + rc = ShClX11ThreadStop(&X11Ctx); + AssertRCReturn(rc, 1); + ShClX11Destroy(&X11Ctx); + return RTTestSummaryAndDestroy(hTest); +} + diff --git a/src/VBox/GuestHost/SharedClipboard/testcase/tstClipboardQt/Makefile.kmk b/src/VBox/GuestHost/SharedClipboard/testcase/tstClipboardQt/Makefile.kmk new file mode 100644 index 00000000..bd9401a3 --- /dev/null +++ b/src/VBox/GuestHost/SharedClipboard/testcase/tstClipboardQt/Makefile.kmk @@ -0,0 +1,52 @@ +# $Id: Makefile.kmk $ +## @file +# Sub-Makefile for tstClipboardQt. +# + +# +# Copyright (C) 2011-2023 Oracle and/or its affiliates. +# +# This file is part of VirtualBox base platform packages, as +# available from https://www.virtualbox.org. +# +# 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, in version 3 of the +# License. +# +# 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, see . +# +# SPDX-License-Identifier: GPL-3.0-only +# + +SUB_DEPTH = ../../../../../.. +include $(KBUILD_PATH)/subheader.kmk + +# +# Targets and units. +# +USES += qt5 +PROGRAMS += tstClipboardQt + +# +# tstClipboardQt +# +tstClipboardQt_TEMPLATE = VBoxQtGuiExe +tstClipboardQt_NAME = tstClipboardQt +tstClipboardQt_CXXFLAGS = \ + $(VBOX_GCC_Wno-implicit-fallthrough) \ + $(VBOX_GCC_Wno-deprecated-declarations) + +tstClipboardQt_SOURCES = \ + tstClipboardQt.cpp + +# The Qt modules we're using. +tstClipboardQt_QT_MODULES = Core Gui Widgets PrintSupport + +include $(FILE_KBUILD_SUB_FOOTER) diff --git a/src/VBox/GuestHost/SharedClipboard/testcase/tstClipboardQt/tstClipboardQt.cpp b/src/VBox/GuestHost/SharedClipboard/testcase/tstClipboardQt/tstClipboardQt.cpp new file mode 100644 index 00000000..ebf3e8fb --- /dev/null +++ b/src/VBox/GuestHost/SharedClipboard/testcase/tstClipboardQt/tstClipboardQt.cpp @@ -0,0 +1,47 @@ +/* $Id: tstClipboardQt.cpp $ */ +/** @file + * Shared Clipboard guest/host Qt code test cases. + */ + +/* + * Copyright (C) 2011-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include +#include +#include +#include +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + QMainWindow window; + QWidget *pWidget = new QWidget; + window.setCentralWidget(pWidget); + QHBoxLayout *pLayout = new QHBoxLayout(pWidget); + + QTextEdit *pTextEdit = new QTextEdit; + pLayout->addWidget(pTextEdit); + + + window.show(); + + app.exec(); +} -- cgit v1.2.3