diff --git a/dll/win32/shell32/CMakeLists.txt b/dll/win32/shell32/CMakeLists.txt index 4ebe876caea..2b1f658bacd 100644 --- a/dll/win32/shell32/CMakeLists.txt +++ b/dll/win32/shell32/CMakeLists.txt @@ -53,6 +53,7 @@ list(APPEND SOURCE droptargets/CexeDropHandler.cpp droptargets/CFSDropTarget.cpp droptargets/CRecyclerDropTarget.cpp + shldataobject.cpp shlexec.cpp shlfileop.cpp shlfolder.cpp diff --git a/dll/win32/shell32/shldataobject.cpp b/dll/win32/shell32/shldataobject.cpp new file mode 100644 index 00000000000..4b6d3186271 --- /dev/null +++ b/dll/win32/shell32/shldataobject.cpp @@ -0,0 +1,103 @@ +/* + * PROJECT: shell32 + * LICENSE: LGPL-2.1-or-later (https://spdx.org/licenses/LGPL-2.1-or-later) + * PURPOSE: SHGetAttributesFromDataObject implementation + * COPYRIGHT: Copyright 2021 Mark Jansen + */ + + +#include "precomp.h" + +WINE_DEFAULT_DEBUG_CHANNEL(shell); + + +static CLIPFORMAT g_DataObjectAttributes = 0; +static const DWORD dwDefaultAttributeMask = SFGAO_CANCOPY | SFGAO_CANMOVE | SFGAO_STORAGE | SFGAO_CANRENAME | + SFGAO_CANDELETE | SFGAO_READONLY | SFGAO_STREAM | SFGAO_FOLDER; + +struct DataObjectAttributes +{ + DWORD dwMask; + DWORD dwAttributes; + UINT cItems; +}; + +static_assert(sizeof(DataObjectAttributes) == 0xc, "Unexpected struct size!"); + + +static +HRESULT _BindToObject(PCUIDLIST_ABSOLUTE pidl, CComPtr& spFolder) +{ + CComPtr spDesktop; + HRESULT hr = SHGetDesktopFolder(&spDesktop); + if (FAILED(hr)) + return hr; + + return spDesktop->BindToObject(pidl, NULL, IID_PPV_ARG(IShellFolder, &spFolder)); +} + +EXTERN_C +HRESULT WINAPI SHGetAttributesFromDataObject(IDataObject* pDataObject, DWORD dwAttributeMask, DWORD* pdwAttributes, UINT* pcItems) +{ + DWORD dwAttributes = 0; + DWORD cItems = 0; + HRESULT hr = S_OK; + + TRACE("(%p, 0x%x, %p, %p)\n", pDataObject, dwAttributeMask, pdwAttributes, pcItems); + + if (!g_DataObjectAttributes) + g_DataObjectAttributes = (CLIPFORMAT)RegisterClipboardFormatW(L"DataObjectAttributes"); + + if (pDataObject) + { + DataObjectAttributes data = {}; + if (FAILED(DataObject_GetData(pDataObject, g_DataObjectAttributes, &data, sizeof(data)))) + { + TRACE("No attributes yet, creating new\n"); + memset(&data, 0, sizeof(data)); + } + + DWORD dwQueryAttributes = dwAttributeMask | dwDefaultAttributeMask; + + if ((data.dwMask & dwQueryAttributes) != dwQueryAttributes) + { + CDataObjectHIDA hida(pDataObject); + CComPtr spFolder; + + if (!FAILED_UNEXPECTEDLY(hr = hida.hr()) && + !FAILED_UNEXPECTEDLY(hr = _BindToObject(HIDA_GetPIDLFolder(hida), spFolder))) + { + CSimpleArray apidl; + for (UINT n = 0; n < hida->cidl; ++n) + { + apidl.Add(HIDA_GetPIDLItem(hida, n)); + } + + SFGAOF rgfInOut = dwQueryAttributes; + hr = spFolder->GetAttributesOf(apidl.GetSize(), apidl.GetData(), &rgfInOut); + if (!FAILED_UNEXPECTEDLY(hr)) + { + data.dwMask = dwQueryAttributes; + // Only store what we asked for + data.dwAttributes = rgfInOut & dwQueryAttributes; + data.cItems = apidl.GetSize(); + + hr = DataObject_SetData(pDataObject, g_DataObjectAttributes, &data, sizeof(data)); + FAILED_UNEXPECTEDLY(hr); + } + } + } + + // Only give the user what they asked for, not everything else we have! + dwAttributes = data.dwAttributes & dwAttributeMask; + cItems = data.cItems; + } + + if (pdwAttributes) + *pdwAttributes = dwAttributes; + + if (pcItems) + *pcItems = cItems; + + return hr; +} diff --git a/dll/win32/shell32/stubs.cpp b/dll/win32/shell32/stubs.cpp index a5061ad1a9d..2d49322a184 100644 --- a/dll/win32/shell32/stubs.cpp +++ b/dll/win32/shell32/stubs.cpp @@ -1302,20 +1302,6 @@ DWORD WINAPI SHGetComputerDisplayNameW(DWORD param1, DWORD param2, DWORD param3, return E_FAIL; } -/* - * Unimplemented - */ -EXTERN_C HRESULT -WINAPI -SHGetAttributesFromDataObject(IDataObject *pdo, - DWORD dwAttributeMask, - DWORD *pdwAttributes, - UINT *pcItems) -{ - FIXME("SHGetAttributesFromDataObject() stub\n"); - return E_NOTIMPL; -} - /* * Unimplemented */ diff --git a/modules/rostests/apitests/shell32/CMakeLists.txt b/modules/rostests/apitests/shell32/CMakeLists.txt index 990e1463d48..7a175032619 100644 --- a/modules/rostests/apitests/shell32/CMakeLists.txt +++ b/modules/rostests/apitests/shell32/CMakeLists.txt @@ -27,6 +27,7 @@ list(APPEND SOURCE ShellExecuteW.cpp ShellHook.cpp ShellState.cpp + SHGetAttributesFromDataObject.cpp SHLimitInputEdit.cpp menu.cpp shelltest.cpp) diff --git a/modules/rostests/apitests/shell32/SHGetAttributesFromDataObject.cpp b/modules/rostests/apitests/shell32/SHGetAttributesFromDataObject.cpp new file mode 100644 index 00000000000..937045cdc25 --- /dev/null +++ b/modules/rostests/apitests/shell32/SHGetAttributesFromDataObject.cpp @@ -0,0 +1,319 @@ +/* + * PROJECT: ReactOS api tests + * LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later) + * PURPOSE: Test for SHGetAttributesFromDataObject + * COPYRIGHT: Copyright 2021 Mark Jansen + */ + +#include "shelltest.h" +#include +#include +#include +#include + + +static CLIPFORMAT g_DataObjectAttributes = 0; +static const DWORD dwDefaultAttributeMask = SFGAO_CANCOPY | SFGAO_CANMOVE | SFGAO_STORAGE | SFGAO_CANRENAME | SFGAO_CANDELETE | + SFGAO_READONLY | SFGAO_STREAM | SFGAO_FOLDER; +static_assert(dwDefaultAttributeMask == 0x2044003B, "Unexpected default attribute mask"); + + +struct TmpFile +{ + WCHAR Buffer[MAX_PATH] = {}; + + void Create(LPCWSTR Folder) + { + GetTempFileNameW(Folder, L"SHG", 0, Buffer); + } + + ~TmpFile() + { + if (Buffer[0]) + { + SetFileAttributesW(Buffer, FILE_ATTRIBUTE_NORMAL); + DeleteFileW(Buffer); + } + } +}; + + +CComPtr _BindToObject(PCUIDLIST_ABSOLUTE pidl) +{ + CComPtr spDesktop, spResult; + HRESULT hr = SHGetDesktopFolder(&spDesktop); + if (FAILED_UNEXPECTEDLY(hr)) + return spResult; + + if (FAILED_UNEXPECTEDLY(spDesktop->BindToObject(pidl, NULL, IID_PPV_ARG(IShellFolder, &spResult)))) + { + spResult.Release(); + } + return spResult; +} + + +static void ok_attributes_(IDataObject* pDataObject, HRESULT expect_hr, DWORD expect_mask, DWORD expect_attr, UINT expect_items) +{ + FORMATETC fmt = { g_DataObjectAttributes, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + STGMEDIUM medium = {}; + + HRESULT hr = pDataObject->GetData(&fmt, &medium); + winetest_ok(hr == expect_hr, "Unexpected result from GetData, got 0x%lx, expected 0x%lx\n", hr, expect_hr); + + if (hr == expect_hr && expect_hr == S_OK) + { + LPVOID blob = GlobalLock(medium.hGlobal); + winetest_ok(blob != nullptr, "Failed to lock hGlobal\n"); + if (blob) + { + SIZE_T size = GlobalSize(medium.hGlobal); + winetest_ok(size == 0xc, "Unexpected size, got %lu, expected 12\n", size); + if (size == 0xc) + { + PDWORD data = (PDWORD)blob; + winetest_ok(data[0] == expect_mask, "Unexpected mask, got 0x%lx, expected 0x%lx\n", data[0], expect_mask); + winetest_ok(data[1] == expect_attr, "Unexpected attr, got 0x%lx, expected 0x%lx\n", data[1], expect_attr); + winetest_ok(data[2] == expect_items, "Unexpected item count, got %lu, expected %u\n", data[2], expect_items); + } + GlobalUnlock(medium.hGlobal); + } + } + + if (SUCCEEDED(hr)) + ReleaseStgMedium(&medium); +} + + +#define ok_attributes (winetest_set_location(__FILE__, __LINE__), 0) ? (void)0 : ok_attributes_ +#define ok_hr_ret(x, y) ok_hr(x, y); if (x != y) return + +static void test_SpecialCases() +{ + DWORD dwAttributeMask = 0, dwAttributes = 123; + UINT cItems = 123; + + HRESULT hr = SHGetAttributesFromDataObject(nullptr, dwAttributeMask, &dwAttributes, &cItems); + ok_hr(hr, S_OK); + ok_int(dwAttributes, 0); + ok_int(cItems, 0); + + cItems = 123; + hr = SHGetAttributesFromDataObject(nullptr, dwAttributeMask, nullptr, &cItems); + ok_hr(hr, S_OK); + ok_int(cItems, 0); + + dwAttributes = 123; + hr = SHGetAttributesFromDataObject(nullptr, dwAttributeMask, &dwAttributes, nullptr); + ok_hr(hr, S_OK); + ok_int(dwAttributes, 0); +} + + +static void test_AttributesRegistration() +{ + WCHAR Buffer[MAX_PATH] = {}; + + GetModuleFileNameW(NULL, Buffer, _countof(Buffer)); + CComHeapPtr spPath(ILCreateFromPathW(Buffer)); + + ok(spPath != nullptr, "Unable to create pidl from %S\n", Buffer); + if (spPath == nullptr) + return; + + SFGAOF attributes = dwDefaultAttributeMask; + HRESULT hr; + { + CComPtr spFolder; + PCUITEMID_CHILD child; + hr = SHBindToParent(spPath, IID_PPV_ARG(IShellFolder, &spFolder), &child); + ok_hr_ret(hr, S_OK); + + hr = spFolder->GetAttributesOf(1, &child, &attributes); + ok_hr_ret(hr, S_OK); + + attributes &= dwDefaultAttributeMask; + } + + CComHeapPtr parent(ILClone(spPath)); + PCIDLIST_RELATIVE child = ILFindLastID(spPath); + ILRemoveLastID(parent); + + CComPtr spDataObject; + hr = CIDLData_CreateFromIDArray(parent, 1, &child, &spDataObject); + ok_hr_ret(hr, S_OK); + + /* Not registered yet */ + ok_attributes(spDataObject, DV_E_FORMATETC, 0, 0, 0); + + /* Ask for attributes, without specifying any */ + DWORD dwAttributeMask = 0, dwAttributes = 0; + UINT cItems = 0; + hr = SHGetAttributesFromDataObject(spDataObject, dwAttributeMask, &dwAttributes, &cItems); + ok_hr(hr, S_OK); + + /* Now there are attributes registered */ + ok_attributes(spDataObject, S_OK, dwDefaultAttributeMask, attributes, 1); + + // Now add an additional mask value (our exe should have a propsheet!) + dwAttributeMask = SFGAO_HASPROPSHEET; + dwAttributes = 0; + cItems = 0; + hr = SHGetAttributesFromDataObject(spDataObject, dwAttributeMask, &dwAttributes, &cItems); + ok_hr(hr, S_OK); + + // Observe that this is now also cached + ok_attributes(spDataObject, S_OK, dwDefaultAttributeMask | SFGAO_HASPROPSHEET, attributes | SFGAO_HASPROPSHEET, 1); +} + +static void test_MultipleFiles() +{ + TmpFile TmpFile1, TmpFile2, TmpFile3; + + CComHeapPtr pidl_tmpfolder; + CComHeapPtr pidl1, pidl2, pidl3; + + ITEMIDLIST* items[3] = {}; + SFGAOF attributes_first = dwDefaultAttributeMask; + SFGAOF attributes2 = dwDefaultAttributeMask; + SFGAOF attributes3 = dwDefaultAttributeMask; + SFGAOF attributes_last = dwDefaultAttributeMask; + + HRESULT hr; + { + WCHAR TempFolder[MAX_PATH] = {}; + GetTempPathW(_countof(TempFolder), TempFolder); + + // Create temp files + TmpFile1.Create(TempFolder); + TmpFile2.Create(TempFolder); + TmpFile3.Create(TempFolder); + + // Last file is read-only + SetFileAttributesW(TmpFile3.Buffer, FILE_ATTRIBUTE_READONLY); + + hr = SHParseDisplayName(TempFolder, NULL, &pidl_tmpfolder, NULL, NULL); + ok_hr_ret(hr, S_OK); + + CComPtr spFolder = _BindToObject(pidl_tmpfolder); + ok(!!spFolder, "Unable to bind to tmp folder\n"); + if (!spFolder) + return; + + hr = spFolder->ParseDisplayName(NULL, 0, PathFindFileNameW(TmpFile1.Buffer), NULL, &pidl1, NULL); + ok_hr_ret(hr, S_OK); + + hr = spFolder->ParseDisplayName(NULL, 0, PathFindFileNameW(TmpFile2.Buffer), NULL, &pidl2, NULL); + ok_hr_ret(hr, S_OK); + + hr = spFolder->ParseDisplayName(NULL, 0, PathFindFileNameW(TmpFile3.Buffer), NULL, &pidl3, NULL); + ok_hr_ret(hr, S_OK); + + items[0] = pidl1; + items[1] = pidl2; + items[2] = pidl3; + + // Query file attributes + hr = spFolder->GetAttributesOf(1, items, &attributes_first); + ok_hr(hr, S_OK); + + hr = spFolder->GetAttributesOf(2, items, &attributes2); + ok_hr(hr, S_OK); + + hr = spFolder->GetAttributesOf(3, items, &attributes3); + ok_hr(hr, S_OK); + + hr = spFolder->GetAttributesOf(1, items + 2, &attributes_last); + ok_hr(hr, S_OK); + + // Ignore any non-default attributes + attributes_first &= dwDefaultAttributeMask; + attributes2 &= dwDefaultAttributeMask; + attributes3 &= dwDefaultAttributeMask; + attributes_last &= dwDefaultAttributeMask; + } + + // Only 'single' files have the stream attribute set + ok(attributes_first & SFGAO_STREAM, "Expected SFGAO_STREAM on attributes_first (0x%lx)\n", attributes_first); + ok(!(attributes2 & SFGAO_STREAM), "Expected no SFGAO_STREAM on attributes2 (0x%lx)\n", attributes2); + ok(!(attributes3 & SFGAO_STREAM), "Expected no SFGAO_STREAM on attributes3 (0x%lx)\n", attributes3); + ok(attributes_last & SFGAO_STREAM, "Expected SFGAO_STREAM on attributes_last (0x%lx)\n", attributes_last); + + // Only attributes common on all are returned, so only the last has the readonly bit set! + ok(!(attributes_first & SFGAO_READONLY), "Expected no SFGAO_READONLY on attributes_first (0x%lx)\n", attributes_first); + ok(!(attributes2 & SFGAO_READONLY), "Expected no SFGAO_READONLY on attributes2 (0x%lx)\n", attributes2); + ok(!(attributes3 & SFGAO_READONLY), "Expected no SFGAO_READONLY on attributes3 (0x%lx)\n", attributes3); + ok(attributes_last & SFGAO_READONLY, "Expected SFGAO_READONLY on attributes_last (0x%lx)\n", attributes_last); + + // The actual tests + { + // Just the first file + CComPtr spDataObject; + hr = CIDLData_CreateFromIDArray(pidl_tmpfolder, 1, items, &spDataObject); + ok_hr_ret(hr, S_OK); + + DWORD dwAttributeMask = 0, dwAttributes = 123; + UINT cItems = 123; + hr = SHGetAttributesFromDataObject(spDataObject, dwAttributeMask, &dwAttributes, &cItems); + ok_hr(hr, S_OK); + ok_attributes(spDataObject, S_OK, dwDefaultAttributeMask, attributes_first, 1); + } + + { + // First 2 files + CComPtr spDataObject; + hr = CIDLData_CreateFromIDArray(pidl_tmpfolder, 2, items, &spDataObject); + ok_hr_ret(hr, S_OK); + + DWORD dwAttributeMask = 0, dwAttributes = 123; + UINT cItems = 123; + hr = SHGetAttributesFromDataObject(spDataObject, dwAttributeMask, &dwAttributes, &cItems); + ok_hr(hr, S_OK); + ok_attributes(spDataObject, S_OK, dwDefaultAttributeMask, attributes2, 2); + } + + { + // All 3 files + CComPtr spDataObject; + hr = CIDLData_CreateFromIDArray(pidl_tmpfolder, 3, items, &spDataObject); + ok_hr_ret(hr, S_OK); + + DWORD dwAttributeMask = 0, dwAttributes = 123; + UINT cItems = 123; + hr = SHGetAttributesFromDataObject(spDataObject, dwAttributeMask, &dwAttributes, &cItems); + ok_hr(hr, S_OK); + ok_attributes(spDataObject, S_OK, dwDefaultAttributeMask, attributes3, 3); + } + + { + // Only the last file + CComPtr spDataObject; + hr = CIDLData_CreateFromIDArray(pidl_tmpfolder, 1, items + 2, &spDataObject); + ok_hr_ret(hr, S_OK); + + DWORD dwAttributeMask = 0, dwAttributes = 123; + UINT cItems = 123; + hr = SHGetAttributesFromDataObject(spDataObject, dwAttributeMask, &dwAttributes, &cItems); + ok_hr(hr, S_OK); + ok_attributes(spDataObject, S_OK, dwDefaultAttributeMask, attributes_last, 1); + } +} + +START_TEST(SHGetAttributesFromDataObject) +{ + HRESULT hr; + + hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + ok_hr(hr, S_OK); + if (!SUCCEEDED(hr)) + return; + + g_DataObjectAttributes = (CLIPFORMAT)RegisterClipboardFormatW(L"DataObjectAttributes"); + ok(g_DataObjectAttributes != 0, "Unable to register DataObjectAttributes\n"); + + test_SpecialCases(); + test_AttributesRegistration(); + test_MultipleFiles(); + + CoUninitialize(); +} diff --git a/modules/rostests/apitests/shell32/testlist.c b/modules/rostests/apitests/shell32/testlist.c index 01219082ba3..7c306faa1a7 100644 --- a/modules/rostests/apitests/shell32/testlist.c +++ b/modules/rostests/apitests/shell32/testlist.c @@ -28,6 +28,7 @@ extern void func_ShellExecuteEx(void); extern void func_ShellExecuteW(void); extern void func_ShellHook(void); extern void func_ShellState(void); +extern void func_SHGetAttributesFromDataObject(void); extern void func_SHLimitInputEdit(void); extern void func_SHParseDisplayName(void); @@ -58,6 +59,7 @@ const struct test winetest_testlist[] = { "ShellExecuteW", func_ShellExecuteW }, { "ShellHook", func_ShellHook }, { "ShellState", func_ShellState }, + { "SHGetAttributesFromDataObject", func_SHGetAttributesFromDataObject }, { "SHLimitInputEdit", func_SHLimitInputEdit }, { "SHParseDisplayName", func_SHParseDisplayName }, { 0, 0 } diff --git a/sdk/include/psdk/shlobj.h b/sdk/include/psdk/shlobj.h index 422714c7b9d..f8be3e7d299 100644 --- a/sdk/include/psdk/shlobj.h +++ b/sdk/include/psdk/shlobj.h @@ -2476,6 +2476,18 @@ SHRunControlPanel( _In_ LPCWSTR commandLine, _In_opt_ HWND parent); +/**************************************************************************** + * SHGetAttributesFromDataObject + */ + +HRESULT +WINAPI +SHGetAttributesFromDataObject( + _In_opt_ IDataObject* pdo, + DWORD dwAttributeMask, + _Out_opt_ DWORD* pdwAttributes, + _Out_opt_ UINT* pcItems); + /**************************************************************************** * SHOpenWithDialog */ diff --git a/sdk/include/reactos/shellutils.h b/sdk/include/reactos/shellutils.h index ae8f8dd2519..2dcbd8037b0 100644 --- a/sdk/include/reactos/shellutils.h +++ b/sdk/include/reactos/shellutils.h @@ -559,6 +559,71 @@ static inline PCUIDLIST_RELATIVE HIDA_GetPIDLItem(CIDA const* pida, SIZE_T i) #ifdef __cplusplus +DECLSPEC_SELECTANY CLIPFORMAT g_cfHIDA = NULL; + +// Allow to use the HIDA from an IDataObject without copying it +struct CDataObjectHIDA +{ +private: + STGMEDIUM m_medium; + CIDA* m_cida; + HRESULT m_hr; + +public: + explicit CDataObjectHIDA(IDataObject* pDataObject) + : m_cida(nullptr) + { + m_medium.tymed = TYMED_NULL; + + if (g_cfHIDA == NULL) + { + g_cfHIDA = (CLIPFORMAT)RegisterClipboardFormatW(CFSTR_SHELLIDLISTW); + } + FORMATETC fmt = { g_cfHIDA, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + + m_hr = pDataObject->GetData(&fmt, &m_medium); + if (FAILED(m_hr)) + { + m_medium.tymed = TYMED_NULL; + return; + } + + m_cida = (CIDA*)::GlobalLock(m_medium.hGlobal); + if (m_cida == nullptr) + { + m_hr = E_UNEXPECTED; + } + } + + ~CDataObjectHIDA() + { + if (m_cida) + ::GlobalUnlock(m_cida); + + ReleaseStgMedium(&m_medium); + } + + HRESULT hr() const + { + return m_hr; + } + + operator bool() const + { + return m_cida != nullptr; + } + + operator const CIDA* () const + { + return m_cida; + } + + const CIDA* operator->() const + { + return m_cida; + } +}; + inline HRESULT DataObject_GetData(IDataObject* pDataObject, CLIPFORMAT clipformat, PVOID pBuffer, SIZE_T dwBufferSize) {