diff --git a/dll/win32/shlwapi/shlwapi.spec b/dll/win32/shlwapi/shlwapi.spec index 8df8fb606dd..7db1f80e104 100644 --- a/dll/win32/shlwapi/shlwapi.spec +++ b/dll/win32/shlwapi/shlwapi.spec @@ -345,8 +345,8 @@ 345 stdcall -noname SHAnsiToAnsi(str ptr long) 346 stdcall -noname SHUnicodeToUnicode(wstr ptr long) 347 stdcall -noname RegDeleteValueWrapW(long wstr) advapi32.RegDeleteValueW -348 stub -noname SHGetFileDescriptionW -349 stub -noname SHGetFileDescriptionA +348 stdcall -noname SHGetFileDescriptionW(wstr wstr wstr ptr ptr) +349 stdcall -noname SHGetFileDescriptionA(str str str ptr ptr) 350 stdcall -noname GetFileVersionInfoSizeWrapW(wstr ptr) 351 stdcall -noname GetFileVersionInfoWrapW(wstr long long ptr) 352 stdcall -noname VerQueryValueWrapW(ptr wstr ptr ptr) @@ -489,7 +489,7 @@ 489 stdcall -noname GlobalAddAtomWrapW(wstr) kernel32.GlobalAddAtomW 490 stdcall -noname GlobalFindAtomWrapW(wstr) kernel32.GlobalFindAtomW 491 stdcall -noname SHGetShellKey(long long long) -492 stub -noname PrettifyFileDescriptionW +492 stdcall -noname PrettifyFileDescriptionW(wstr wstr) 493 stdcall -noname SHPropertyBag_ReadType(ptr wstr ptr long) 494 stdcall -noname SHPropertyBag_ReadStr(ptr wstr ptr long) 495 stdcall -noname SHPropertyBag_WriteStr(ptr wstr wstr) diff --git a/dll/win32/shlwapi/utils.cpp b/dll/win32/shlwapi/utils.cpp index 6871764c154..a380bec3168 100644 --- a/dll/win32/shlwapi/utils.cpp +++ b/dll/win32/shlwapi/utils.cpp @@ -15,6 +15,7 @@ #define IShellFolder_CompareIDs _disabled_IShellFolder_CompareIDs_ #include "precomp.h" +#include #include #include #include @@ -957,3 +958,239 @@ NextPathW( return (*pchEnd == L';') ? (pchEnd + 1) : pchEnd; } + +static HRESULT +_AllocValueString( + HKEY hkey, + PCWSTR pszSubKey, + PCWSTR pszValue, + PWSTR* ppszOut) +{ + *ppszOut = NULL; + + DWORD cbData; + LSTATUS error = SHGetValueW(hkey, pszSubKey, pszValue, NULL, NULL, &cbData); + if (error) + return HRESULT_FROM_WIN32(error); + + PWSTR pszData = (PWSTR)LocalAlloc(LPTR, cbData); + if (!pszData) + return HRESULT_FROM_WIN32(ERROR_OUTOFMEMORY); + + error = SHGetValueW(hkey, pszSubKey, pszValue, NULL, pszData, &cbData); + if (error) + { + LocalFree(pszData); + return HRESULT_FROM_WIN32(error); + } + + *ppszOut = pszData; + return S_OK; +} + +/************************************************************************* + * PrettifyFileDescriptionW [SHLWAPI.492] + * + * @see SHGetFileDescriptionW + */ +VOID WINAPI +PrettifyFileDescriptionW(_Inout_ PWSTR pszTarget, _In_opt_ PCWSTR pszCutList) +{ + if (!pszTarget || !*pszTarget) + return; + + PWSTR pszFreeList = NULL; + PCWSTR pszList = pszCutList; + PCWSTR pszAssoc = L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileAssociation"; + if (_AllocValueString(HKEY_LOCAL_MACHINE, pszAssoc, L"CutList", &pszFreeList) == S_OK) + pszList = pszFreeList; + + if (pszList && *pszList) + { + for (PCWSTR pszEntry = pszList; *pszEntry; pszEntry += lstrlenW(pszEntry) + 1) + { + PWSTR pszMatch = StrRStrIW(pszTarget, NULL, pszEntry); + if (!pszMatch) + continue; + + if (pszMatch[lstrlenW(pszEntry)]) + continue; + + *pszMatch = UNICODE_NULL; + while (pszMatch > pszTarget && pszMatch[-1] == L' ') + { + --pszMatch; + *pszMatch = UNICODE_NULL; + } + + break; + } + } + + if (pszFreeList) + LocalFree(pszFreeList); +} + +/************************************************************************* + * SHGetFileDescriptionW [SHLWAPI.348] + * + * @see SHGetFileDescriptionA + * @see PrettifyFileDescriptionW + */ +BOOL WINAPI SHGetFileDescriptionW( + _In_ PCWSTR pszPath, + _In_opt_ PCWSTR pszVerKey, + _In_opt_ PCWSTR pszDisplayName, + _Out_opt_ PWSTR pszOut, + _Inout_ PUINT pcchOut) +{ + DWORD pdwAttrs = 0; + if (!PathFileExistsAndAttributesW(pszPath, &pdwAttrs)) + return FALSE; + + WCHAR szPath[MAX_PATH]; + StringCchCopyW(szPath, _countof(szPath), pszPath); + + PVOID pvDescription = NULL; + UINT cchDescription = 0; + PVOID pvBlock = NULL; + + BOOL bIsFile = !(pdwAttrs & FILE_ATTRIBUTE_DIRECTORY) && + !PathIsUNCServerW(pszPath) && + !PathIsUNCServerShareW(pszPath); + if (bIsFile) + { + DWORD dwHandle = 0; + DWORD cbBlock = GetFileVersionInfoSizeW(szPath, &dwHandle); + if (cbBlock) + { + pvBlock = LocalAlloc(LPTR, cbBlock); + if (pvBlock && GetFileVersionInfoW(szPath, dwHandle, cbBlock, pvBlock)) + { + WCHAR szSubBlock[60]; + BOOL ret = FALSE; + if (pszVerKey) + { + StringCchCopyW(szSubBlock, _countof(szSubBlock), pszVerKey); + ret = VerQueryValueW(pvBlock, szSubBlock, &pvDescription, &cchDescription); + } + + if (!ret) + { + PVOID pTranslation = NULL; + UINT cbTranslation = 0; + if (VerQueryValueW(pvBlock, L"\\VarFileInfo\\Translation", &pTranslation, + &cbTranslation) && cbTranslation) + { + UINT langId = ((PWORD)pTranslation)[0]; + UINT codePage = ((PWORD)pTranslation)[1]; + StringCchPrintfW(szSubBlock, _countof(szSubBlock), + L"\\StringFileInfo\\%04X%04X\\FileDescription", + langId, codePage); + ret = VerQueryValueW(pvBlock, szSubBlock, &pvDescription, &cchDescription); + } + } + + if (!ret) + { + // 0x0409: English (United States), 0x04B0: UTF-16 codepage + StringCchCopyW(szSubBlock, _countof(szSubBlock), + L"\\StringFileInfo\\040904B0\\FileDescription"); + ret = VerQueryValueW(pvBlock, szSubBlock, &pvDescription, &cchDescription); + } + if (!ret) + { + // 0x0409: English (United States), 0x04E4: Latin 1 codepage + StringCchCopyW(szSubBlock, _countof(szSubBlock), + L"\\StringFileInfo\\040904E4\\FileDescription"); + ret = VerQueryValueW(pvBlock, szSubBlock, &pvDescription, &cchDescription); + } + if (!ret) + { + // 0x0409: English (United States), 0x0000: Neutral + StringCchCopyW(szSubBlock, _countof(szSubBlock), + L"\\StringFileInfo\\04090000\\FileDescription"); + ret = VerQueryValueW(pvBlock, szSubBlock, &pvDescription, &cchDescription); + } + } + } + } + + PWSTR pszDescription = (PWSTR)pvDescription; + if (!pszDescription || !*pszDescription) + { + PathRemoveExtensionW(szPath); + pszDescription = PathFindFileNameW(szPath); + cchDescription = lstrlenW(pszDescription); + } + + PrettifyFileDescriptionW(pszDescription, pszDisplayName); + + UINT cchResult = lstrlenW(pszDescription) + 1; + if (pszOut) + { + UINT cchCopy = min(cchResult, *pcchOut); + StringCchCopyW(pszOut, cchCopy, pszDescription); + *pcchOut = cchCopy; + } + else + { + *pcchOut = cchResult; + } + + if (pvBlock) + LocalFree(pvBlock); + + return TRUE; +} + +/************************************************************************* + * SHGetFileDescriptionA [SHLWAPI.349] + * + * @see SHGetFileDescriptionW + */ +BOOL WINAPI SHGetFileDescriptionA( + _In_ PCSTR pszPath, + _In_opt_ PCSTR pszVerKey, + _In_opt_ PCSTR pszDisplayName, + _Out_opt_ PSTR pszOut, + _Inout_ PUINT pcchOut) +{ + WCHAR szPathW[MAX_PATH], szVerKeyW[MAX_PATH], szDisplayNameW[MAX_PATH], szOutW[MAX_PATH]; + CHAR szOutA[MAX_PATH]; + BOOL ret; + UINT cchOutW; + + SHAnsiToUnicode(pszPath, szPathW, _countof(szPathW)); + szPathW[_countof(szPathW) - 1] = UNICODE_NULL; + + if (pszVerKey) + { + SHAnsiToUnicode(pszVerKey, szVerKeyW, _countof(szVerKeyW)); + szVerKeyW[_countof(szVerKeyW) - 1] = UNICODE_NULL; + } + + if (pszDisplayName) + { + SHAnsiToUnicode(pszDisplayName, szDisplayNameW, _countof(szDisplayNameW)); + szDisplayNameW[_countof(szDisplayNameW) - 1] = UNICODE_NULL; + } + + cchOutW = (UINT)_countof(szOutW); + ret = SHGetFileDescriptionW(szPathW, (pszVerKey ? szVerKeyW : NULL), + (pszDisplayName ? szDisplayNameW : NULL), szOutW, &cchOutW); + if (ret) + { + szOutW[_countof(szOutW) - 1] = UNICODE_NULL; + + if (!pszOut) + pszOut = szOutA; + + SHUnicodeToAnsi(szOutW, pszOut, *pcchOut); + if (*pcchOut > 0) + pszOut[*pcchOut - 1] = ANSI_NULL; + *pcchOut = lstrlenA(pszOut) + 1; + } + + return ret; +} diff --git a/modules/rostests/apitests/shlwapi/CMakeLists.txt b/modules/rostests/apitests/shlwapi/CMakeLists.txt index af6e57aa622..2359bcd4323 100644 --- a/modules/rostests/apitests/shlwapi/CMakeLists.txt +++ b/modules/rostests/apitests/shlwapi/CMakeLists.txt @@ -21,6 +21,7 @@ list(APPEND SOURCE PathUnExpandEnvStringsForUser.c QuerySourceCreateFromKey.cpp SHAreIconsEqual.c + SHGetFileDescription.c SHGetRestriction.c SHInvokeCommandsOnContextMenu.cpp SHLoadIndirectString.c diff --git a/modules/rostests/apitests/shlwapi/SHGetFileDescription.c b/modules/rostests/apitests/shlwapi/SHGetFileDescription.c new file mode 100644 index 00000000000..4effaaa99a1 --- /dev/null +++ b/modules/rostests/apitests/shlwapi/SHGetFileDescription.c @@ -0,0 +1,159 @@ +/* + * PROJECT: ReactOS api tests + * LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later) + * PURPOSE: Tests for SHGetFileDescriptionA/W + * COPYRIGHT: Copyright 2026 Katayama Hirofumi MZ + */ + +#include +#include +#include + +static WCHAR g_notepad[MAX_PATH]; +static WCHAR g_winver[MAX_PATH]; + +typedef BOOL (WINAPI *FN_SHGetFileDescriptionW)(PCWSTR, PCWSTR, PCWSTR, PWSTR, PUINT); +static FN_SHGetFileDescriptionW g_fnSHGetFileDescriptionW = NULL; + +static BOOL LoadFunctions(void) +{ + HMODULE hSHLWAPI = GetModuleHandleW(L"shlwapi.dll"); + if (!hSHLWAPI) + hSHLWAPI = LoadLibraryW(L"shlwapi.dll"); + if (!hSHLWAPI) + return FALSE; + + g_fnSHGetFileDescriptionW = + (FN_SHGetFileDescriptionW)GetProcAddress(hSHLWAPI, MAKEINTRESOURCEA(348)); + + return g_fnSHGetFileDescriptionW != NULL; +} + +static BOOL BuildSystemPaths(void) +{ + WCHAR sys[MAX_PATH]; + if (!GetSystemDirectoryW(sys, _countof(sys))) + return FALSE; + + StringCchPrintfW(g_notepad, _countof(sys), L"%ls\\notepad.exe", sys); + StringCchPrintfW(g_winver, _countof(sys), L"%ls\\winver.exe", sys); + return PathFileExistsW(g_notepad) && PathFileExistsW(g_winver); +} + +static void TEST_QuerySizeOnly(void) +{ + UINT cch = 0; + BOOL ret = g_fnSHGetFileDescriptionW(g_notepad, NULL, NULL, NULL, &cch); + + ok_int(ret, TRUE); + ok(cch > 0, "cch was %u\n", cch); +} + +static void TEST_GetDescriptionNotepad(void) +{ + WCHAR buf[256] = L""; + UINT cch = _countof(buf); + BOOL ret = g_fnSHGetFileDescriptionW(g_notepad, NULL, NULL, buf, &cch); + + ok_int(ret, TRUE); + ok(buf[0] != UNICODE_NULL, "buf was empty\n"); + ok(0 < cch && cch <= _countof(buf), "cch was %u\n", cch); + trace("buf: %s\n", wine_dbgstr_w(buf)); +} + +static void TEST_GetDescriptionWinver(void) +{ + WCHAR buf[256] = L""; + UINT cch = _countof(buf); + BOOL ret = g_fnSHGetFileDescriptionW(g_winver, NULL, NULL, buf, &cch); + + ok_int(ret, TRUE); + ok(buf[0] != UNICODE_NULL, "buf was empty\n"); + ok(0 < cch && cch <= _countof(buf), "cch was %u\n", cch); + trace("buf: %s\n", wine_dbgstr_w(buf)); +} + +static void TEST_NonExistentFile(void) +{ + WCHAR buf[256] = L""; + UINT cch = _countof(buf); + BOOL ret = g_fnSHGetFileDescriptionW(L"C:\\This\\Does\\Not\\Exist.exe", NULL, NULL, + buf, &cch); + ok_int(ret, FALSE); +} + +static void TEST_DirectoryPath(void) +{ + WCHAR sys[MAX_PATH]; + GetSystemDirectoryW(sys, _countof(sys)); + + WCHAR buf[256] = L""; + UINT cch = _countof(buf); + BOOL ret = g_fnSHGetFileDescriptionW(sys, NULL, NULL, buf, &cch); + + ok_int(ret, TRUE); + ok(_wcsicmp(buf, L"System32") == 0, "buf was %s\n", wine_dbgstr_w(buf)); +} + +static void TEST_TinyBuffer(void) +{ + WCHAR buf[1] = { L'X' }; + UINT cch = 1; + BOOL ret = g_fnSHGetFileDescriptionW(g_notepad, NULL, NULL, buf, &cch); + + ok_int(ret, TRUE); + ok_int(buf[0], UNICODE_NULL); + ok_int(cch, 1); +} + +static void TEST_CustomVerKey(void) +{ + WCHAR buf[256] = L""; + UINT cch = _countof(buf); + BOOL ret = g_fnSHGetFileDescriptionW(g_notepad, + L"\\StringFileInfo\\040904B0\\FileDescription", + NULL, buf, &cch); + ok_int(ret, TRUE); + ok(buf[0] != UNICODE_NULL, "buf was empty\n"); +} + +static void TEST_InvalidVerKey(void) +{ + WCHAR buf[256] = L"", bufNorm[256] = L""; + UINT cch = _countof(buf), cchNorm = _countof(bufNorm); + + BOOL retInvalid = g_fnSHGetFileDescriptionW(g_notepad, + L"\\StringFileInfo\\FFFFFFFF\\NoSuchKey", + NULL, buf, &cch); + BOOL retNormal = g_fnSHGetFileDescriptionW(g_notepad, NULL, NULL, bufNorm, &cchNorm); + + ok_int(retInvalid, TRUE); + ok_int(retNormal, TRUE); + ok(buf[0] != UNICODE_NULL, "buf was empty\n"); + ok(lstrcmpW(buf, bufNorm) == 0, "buf was %s, bufNorm was %s\n", wine_dbgstr_w(buf), + wine_dbgstr_w(bufNorm)); +} + +START_TEST(SHGetFileDescription) +{ + if (!LoadFunctions()) + { + skip("SHGetFileDescription not found\n"); + return; + } + + if (!BuildSystemPaths()) + { + skip("notepad.exe and/or winver.exe not found\n"); + return; + } + + TEST_QuerySizeOnly(); + TEST_GetDescriptionNotepad(); + TEST_GetDescriptionWinver(); + TEST_NonExistentFile(); + TEST_DirectoryPath(); + TEST_TinyBuffer(); + TEST_CustomVerKey(); + TEST_InvalidVerKey(); +} diff --git a/modules/rostests/apitests/shlwapi/testlist.c b/modules/rostests/apitests/shlwapi/testlist.c index 9e9857c7833..39a97ce9d85 100644 --- a/modules/rostests/apitests/shlwapi/testlist.c +++ b/modules/rostests/apitests/shlwapi/testlist.c @@ -17,6 +17,7 @@ extern void func_PathUnExpandEnvStrings(void); extern void func_PathUnExpandEnvStringsForUser(void); extern void func_QuerySourceCreateFromKey(void); extern void func_SHAreIconsEqual(void); +extern void func_SHGetFileDescription(void); extern void func_SHGetRestriction(void); extern void func_SHInvokeCommandsOnContextMenu(void); extern void func_SHLoadIndirectString(void); @@ -44,6 +45,7 @@ const struct test winetest_testlist[] = { "PathUnExpandEnvStringsForUser", func_PathUnExpandEnvStringsForUser }, { "QuerySourceCreateFromKey", func_QuerySourceCreateFromKey }, { "SHAreIconsEqual", func_SHAreIconsEqual }, + { "SHGetFileDescription", func_SHGetFileDescription }, { "SHGetRestriction", func_SHGetRestriction }, { "SHInvokeCommandsOnContextMenu", func_SHInvokeCommandsOnContextMenu }, { "SHLoadIndirectString", func_SHLoadIndirectString }, diff --git a/sdk/include/reactos/shlwapi_undoc.h b/sdk/include/reactos/shlwapi_undoc.h index 37f55f3f0ea..6bff319fa3f 100644 --- a/sdk/include/reactos/shlwapi_undoc.h +++ b/sdk/include/reactos/shlwapi_undoc.h @@ -419,6 +419,22 @@ BOOL WINAPI PathFileExistsAndAttributesA(LPCSTR lpszPath, DWORD* dwAttr); BOOL WINAPI PathFileExistsAndAttributesW(LPCWSTR lpszPath, DWORD* dwAttr); #endif +VOID WINAPI PrettifyFileDescriptionW(_Inout_ PWSTR pszTarget, _In_opt_ PCWSTR pszCutList); + +BOOL WINAPI SHGetFileDescriptionA( + _In_ PCSTR pszPath, + _In_opt_ PCSTR pszVerKey, + _In_opt_ PCSTR pszDisplayName, + _Out_opt_ PSTR pszOut, + _Inout_ PUINT pcchOut); + +BOOL WINAPI SHGetFileDescriptionW( + _In_ PCWSTR pszPath, + _In_opt_ PCWSTR pszVerKey, + _In_opt_ PCWSTR pszDisplayName, + _Out_opt_ PWSTR pszOut, + _Inout_ PUINT pcchOut); + LPSTR WINAPI StrCpyNXA(LPSTR lpszDest, LPCSTR lpszSrc, int iLen); LPWSTR WINAPI StrCpyNXW(LPWSTR lpszDest, LPCWSTR lpszSrc, int iLen); @@ -426,10 +442,12 @@ LPWSTR WINAPI StrCpyNXW(LPWSTR lpszDest, LPCWSTR lpszSrc, int iLen); #define PathIsValidChar PathIsValidCharW #define StrCpyNX StrCpyNXW #define FixSlashesAndColon FixSlashesAndColonW + #define SHGetFileDescription SHGetFileDescriptionW #else #define PathIsValidChar PathIsValidCharA #define StrCpyNX StrCpyNXA #define FixSlashesAndColon FixSlashesAndColonA + #define SHGetFileDescription SHGetFileDescriptionA #endif BOOL WINAPI