diff --git a/dll/win32/shell32/wine/shellpath.c b/dll/win32/shell32/wine/shellpath.c index e55b48b1c33..4ab83c1f317 100644 --- a/dll/win32/shell32/wine/shellpath.c +++ b/dll/win32/shell32/wine/shellpath.c @@ -665,6 +665,184 @@ static BOOL PathMakeUniqueNameA( /************************************************************************* * PathMakeUniqueNameW [internal] */ +#ifdef __REACTOS__ +/* https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-pathmakeuniquename */ +static BOOL PathMakeUniqueNameW( + _Out_ PWSTR pszUniqueName, + _In_ UINT cchMax, + _In_ PCWSTR pszTemplate, + _In_opt_ PCWSTR pszLongPlate, + _In_opt_ PCWSTR pszDir) +{ + TRACE("%p %u %s %s %s\n", + pszUniqueName, cchMax, debugstr_w(pszTemplate), + debugstr_w(pszLongPlate), debugstr_w(pszDir)); + + if (!cchMax || !pszUniqueName) + return FALSE; + + pszUniqueName[0] = UNICODE_NULL; + + LPWSTR pszDest = pszUniqueName; + UINT dirLength = 0; + if (pszDir) + { + if (StringCchCopyW(pszUniqueName, cchMax - 1, pszDir) != S_OK) + return FALSE; + + pszDest = PathAddBackslashW(pszUniqueName); + if (!pszDest) + return FALSE; + + dirLength = lstrlenW(pszDir); + } + + PCWSTR pszTitle = pszLongPlate ? pszLongPlate : pszTemplate; + + if ( !pszTitle + || !IsLFNDriveW(pszDir) +#if (NTDDI_VERSION < NTDDI_VISTA) + || pszDir +#endif + ) + { + if (!pszTemplate) + return FALSE; + + PCWSTR pszSrc = pszTemplate; + PCWSTR pchDotExt = PathFindExtensionW(pszTemplate); + INT extLength = lstrlenW(pchDotExt); + + WCHAR formatString[MAX_PATH]; + if (StringCchCopyW(formatString, _countof(formatString), L"%d") != S_OK) + return FALSE; + + INT cchTitle = pchDotExt - pszTemplate; + if (cchTitle > 1) + { + PCWSTR pch = pchDotExt - 1; + while (cchTitle > 1 && (L'0' <= *pch && *pch <= L'9')) + { + --cchTitle; + --pch; + } + } + + if (cchTitle > 7) + cchTitle = 7; + + while (extLength + cchTitle + dirLength + 1 > (cchMax - 1) && cchTitle > 1) + --cchTitle; + + INT maxCount; + if (cchTitle >= 1) + maxCount = (cchTitle != 1) ? 100 : 10; + else + maxCount = 1; + + if (StringCchCopyNW(pszDest, cchMax - dirLength, pszSrc, cchTitle) != S_OK) + return FALSE; + + LPWSTR pchTitle = pszDest + cchTitle; + INT ich; + for (ich = 1; ich < maxCount; ++ich) + { + WCHAR tempName[MAX_PATH]; + if (StringCchPrintfW(tempName, _countof(tempName), formatString, ich) != S_OK || + StringCchCatW(tempName, _countof(tempName), pchDotExt) != S_OK) + { + return FALSE; + } + + if (StringCchCopyW(pchTitle, cchMax - (pchTitle - pszUniqueName), tempName) != S_OK) + return FALSE; + + if (!PathFileExistsW(pszUniqueName)) + return TRUE; + } + } + else + { + PCWSTR openParen = StrChrW(pszTitle, L'('); + PCWSTR pchDotExt = NULL; + INT cchTitle = 0; + WCHAR formatString[MAX_PATH]; + + if (openParen) + { + while (openParen) + { + PCWSTR pch = openParen + 1; + while (*pch >= L'0' && *pch <= L'9') + ++pch; + + if (*pch == L')') + break; + + openParen = StrChrW(openParen + 1, L'('); + } + + if (openParen) + { + pchDotExt = openParen + 1; + cchTitle = pchDotExt - pszTitle; + if (StringCchCopyW(formatString, _countof(formatString), L"%d") != S_OK) + return FALSE; + } + else + { + pchDotExt = PathFindExtensionW(pszTitle); + cchTitle = pchDotExt - pszTitle; + if (StringCchCopyW(formatString, _countof(formatString), L" (%d)") != S_OK) + return FALSE; + } + } + else + { + pchDotExt = PathFindExtensionW(pszTitle); + cchTitle = pchDotExt - pszTitle; + if (StringCchCopyW(formatString, _countof(formatString), L" (%d)") != S_OK) + return FALSE; + } + + INT maxCount = 1; + INT remainingChars = cchMax - cchTitle - dirLength - lstrlenW(formatString) + 2; + + if (remainingChars == 1) + maxCount = 10; + else if (remainingChars == 2) + maxCount = 100; + else if (remainingChars > 0) + maxCount = 1000; + else + maxCount = 1; + + if (StringCchCopyNW(pszDest, cchMax - dirLength, pszTitle, cchTitle) != S_OK) + return FALSE; + + LPWSTR pchTitle = pszDest + cchTitle; + INT ich; + for (ich = 1; ich < maxCount; ++ich) + { + WCHAR tempName[MAX_PATH]; + if (StringCchPrintfW(tempName, _countof(tempName), formatString, ich) != S_OK || + StringCchCatW(tempName, _countof(tempName), pchDotExt) != S_OK) + { + return FALSE; + } + + if (StringCchCopyW(pchTitle, cchMax - (pchTitle - pszUniqueName), tempName) != S_OK) + return FALSE; + + if (!PathFileExistsW(pszUniqueName)) + return TRUE; + } + } + + pszUniqueName[0] = UNICODE_NULL; + return FALSE; +} +#else static BOOL PathMakeUniqueNameW( LPWSTR lpszBuffer, DWORD dwBuffSize, @@ -677,6 +855,7 @@ static BOOL PathMakeUniqueNameW( debugstr_w(lpszLongName), debugstr_w(lpszPathName)); return TRUE; } +#endif /************************************************************************* * PathMakeUniqueName [SHELL32.47] diff --git a/modules/rostests/apitests/shell32/CMakeLists.txt b/modules/rostests/apitests/shell32/CMakeLists.txt index 5389915fd87..491a7e65017 100644 --- a/modules/rostests/apitests/shell32/CMakeLists.txt +++ b/modules/rostests/apitests/shell32/CMakeLists.txt @@ -24,6 +24,7 @@ list(APPEND SOURCE OpenAs_RunDLL.cpp PathIsEqualOrSubFolder.cpp PathIsTemporary.cpp + PathMakeUniqueName.cpp PathResolve.cpp RealShellExecuteEx.cpp SHAppBarMessage.cpp diff --git a/modules/rostests/apitests/shell32/PathMakeUniqueName.cpp b/modules/rostests/apitests/shell32/PathMakeUniqueName.cpp new file mode 100644 index 00000000000..abd0c7bed11 --- /dev/null +++ b/modules/rostests/apitests/shell32/PathMakeUniqueName.cpp @@ -0,0 +1,119 @@ +/* + * PROJECT: ReactOS api tests + * LICENSE: LGPL-2.1-or-later (https://spdx.org/licenses/LGPL-2.1-or-later) + * PURPOSE: Tests for PathMakeUniqueName + * COPYRIGHT: Copyright 2025 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com) + */ + +#include "shelltest.h" +#include +#include + +#define ok_wstri(x, y) \ + ok(_wcsicmp(x, y) == 0, "Wrong string. Expected '%S', got '%S'\n", y, x) + +/* IsLFNDriveW */ +typedef BOOL (WINAPI *FN_IsLFNDriveW)(LPCWSTR); + +START_TEST(PathMakeUniqueName) +{ + WCHAR szPath[MAX_PATH], szCurDir[MAX_PATH], szTempDir[MAX_PATH]; + BOOL result, bUseLong = FALSE; + FN_IsLFNDriveW pIsLFNDriveW = + (FN_IsLFNDriveW)GetProcAddress(GetModuleHandleW(L"shell32"), MAKEINTRESOURCEA(42)); + + // Move to temporary folder + GetCurrentDirectoryW(_countof(szCurDir), szCurDir); + GetEnvironmentVariableW(L"TEMP", szTempDir, _countof(szTempDir)); + SetCurrentDirectoryW(szTempDir); + + if (pIsLFNDriveW) + bUseLong = pIsLFNDriveW(szTempDir) && IsWindowsVistaOrGreater(); + trace("bUseLong: %d\n", bUseLong); + + DeleteFileW(L"test.txt"); + + // Test 1: Basic operation + szPath[0] = UNICODE_NULL; + result = PathMakeUniqueName(szPath, _countof(szPath), L"test.txt", NULL, NULL); + ok_int(result, TRUE); + ok_wstri(szPath, L"test (1).txt"); + + // Test 2: Specify directory + szPath[0] = UNICODE_NULL; + result = PathMakeUniqueName(szPath, _countof(szPath), L"test.txt", NULL, L"."); + ok_int(result, TRUE); + ok_wstri(szPath, (bUseLong ? L".\\test (1).txt" : L".\\test1.txt")); + + // Test 3: Duplicated filename + CreateFileW(L"test.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + szPath[0] = UNICODE_NULL; + result = PathMakeUniqueName(szPath, _countof(szPath), L"test.txt", NULL, NULL); + ok_int(result, TRUE); + ok_wstri(szPath, L"test (1).txt"); + DeleteFileW(L"test.txt"); + + // Build long name + WCHAR longName[MAX_PATH + 32]; + for (auto& ch : longName) + ch = L'A'; + longName[_countof(longName) - 10] = UNICODE_NULL; + lstrcatW(longName, L".txt"); + + // Test 4: Long filename + result = PathMakeUniqueName(szPath, _countof(szPath), longName, NULL, NULL); + szPath[0] = UNICODE_NULL; + ok_int(result, FALSE); + ok_wstri(szPath, L""); + + // Test 5: Invalid parameter + szPath[0] = UNICODE_NULL; + result = PathMakeUniqueName(NULL, 0, L"test.txt", NULL, NULL); + ok_int(result, FALSE); + + // Test 6: Template and longplate + szPath[0] = UNICODE_NULL; + result = PathMakeUniqueName(szPath, _countof(szPath), L"template.txt", L"longplate.txt", NULL); + ok_int(result, TRUE); + ok_wstri(szPath, L"longplate (1).txt"); + + // Test 7: Template only + szPath[0] = UNICODE_NULL; + result = PathMakeUniqueName(szPath, _countof(szPath), L"template.txt", NULL, NULL); + ok_int(result, TRUE); + ok_wstri(szPath, L"template (1).txt"); + + // Test 8: Folder and duplicated filename + CreateFileW(L".\\temp\\test.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + szPath[0] = UNICODE_NULL; + result = PathMakeUniqueName(szPath, _countof(szPath), L"test.txt", NULL, L"."); + ok_int(result, TRUE); + ok_wstri(szPath, (bUseLong ? L".\\test (1).txt" : L".\\test1.txt")); + DeleteFileW(L".\\test.txt"); + + // Test 9: Test extension + CreateFileW(L".\\test.hoge", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + szPath[0] = UNICODE_NULL; + result = PathMakeUniqueName(szPath, _countof(szPath), L"test.hoge", NULL, L"."); + ok_int(result, TRUE); + ok_wstri(szPath, (bUseLong ? L".\\test (1).hoge" : L".\\test1.hoge")); + DeleteFileW(L".\\test.hoge"); + + // Test 10: Folder in folder + CreateDirectoryW(L".\\hoge", NULL); + szPath[0] = UNICODE_NULL; + result = PathMakeUniqueName(szPath, _countof(szPath), L"test.txt", NULL, L".\\hoge"); + ok_int(result, TRUE); + ok_wstri(szPath, (bUseLong ? L".\\hoge\\test (1).txt" : L".\\hoge\\test1.txt")); + RemoveDirectoryW(L".\\hoge"); + + // Test 11: File in folder + CreateFileW(L".\\hoge.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + szPath[0] = UNICODE_NULL; + result = PathMakeUniqueName(szPath, _countof(szPath), L"test.txt", NULL, L"."); + ok_int(result, TRUE); + ok_wstri(szPath, (bUseLong ? L".\\test (1).txt" : L".\\test1.txt")); + DeleteFileW(L".\\hoge.txt"); + + SetCurrentDirectoryW(szCurDir); +} diff --git a/modules/rostests/apitests/shell32/testlist.c b/modules/rostests/apitests/shell32/testlist.c index cc5398ddb68..f76eb744a5e 100644 --- a/modules/rostests/apitests/shell32/testlist.c +++ b/modules/rostests/apitests/shell32/testlist.c @@ -26,6 +26,7 @@ extern void func_menu(void); extern void func_OpenAs_RunDLL(void); extern void func_PathIsEqualOrSubFolder(void); extern void func_PathIsTemporary(void); +extern void func_PathMakeUniqueName(void); extern void func_PathResolve(void); extern void func_PIDL(void); extern void func_RealShellExecuteEx(void); @@ -82,6 +83,7 @@ const struct test winetest_testlist[] = { "OpenAs_RunDLL", func_OpenAs_RunDLL }, { "PathIsEqualOrSubFolder", func_PathIsEqualOrSubFolder }, { "PathIsTemporary", func_PathIsTemporary }, + { "PathMakeUniqueName", func_PathMakeUniqueName }, { "PathResolve", func_PathResolve }, { "PIDL", func_PIDL }, { "RealShellExecuteEx", func_RealShellExecuteEx }, diff --git a/sdk/include/psdk/shlobj.h b/sdk/include/psdk/shlobj.h index 10e73f0ddcd..530b9c07f9b 100644 --- a/sdk/include/psdk/shlobj.h +++ b/sdk/include/psdk/shlobj.h @@ -409,6 +409,14 @@ SHStartNetConnectionDialogW( _In_ LPCWSTR pszRemoteName, _In_ DWORD dwType); +BOOL WINAPI +PathMakeUniqueName( + _Out_ PWSTR pszUniqueName, + _In_ UINT cchMax, + _In_ PCWSTR pszTemplate, + _In_opt_ PCWSTR pszLongPlate, + _In_opt_ PCWSTR pszDir); + /***************************************************************************** * IContextMenu interface */