1 /* 2 * PROJECT: ReactOS interoperability tests 3 * LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later) 4 * PURPOSE: Formal locale verification tests 5 * COPYRIGHT: Copyright 2024 Stanislav Motylkov <x86corez@gmail.com> 6 * Copyright 2024 Katayama Hirofumi MZ <katayama.hirofumi.mz@gmail.com> 7 */ 8 9 #include "interop.h" 10 11 #include <winnls.h> 12 #include <strsafe.h> 13 #include <shlwapi.h> 14 15 #include <set> 16 #include <map> 17 18 enum E_MODULE 19 { 20 shell32, 21 userenv, 22 syssetup, 23 mmsys, 24 explorer_old, 25 }; 26 27 enum E_STRING 28 { 29 SH32_PROGRAMS, 30 SH32_STARTUP, 31 SH32_STARTMENU, 32 SH32_PROGRAM_FILES, 33 SH32_PROGRAM_FILES_COMMON, 34 SH32_ADMINTOOLS, 35 UENV_STARTMENU, 36 UENV_PROGRAMS, 37 UENV_STARTUP, 38 SYSS_PROGRAMFILES, 39 SYSS_COMMONFILES, 40 MMSY_STARTMENU, 41 EOLD_PROGRAMS, 42 }; 43 44 typedef struct PART_TEST 45 { 46 E_MODULE eModule; 47 UINT id; 48 SIZE_T nParts; 49 SIZE_T gotParts; 50 } PART_TEST; 51 52 typedef struct PART 53 { 54 E_STRING Num; 55 UINT Idx; 56 } PART; 57 58 typedef struct PART_MATCH 59 { 60 PART p1, p2; 61 } PART_MATCH; 62 63 DWORD dwVersion; 64 LCID curLcid = 0; 65 std::set<LANGID> langs; 66 std::map<E_MODULE, HMODULE> mod; 67 std::map<E_STRING, PART_TEST> parts; 68 69 struct PART_PAIR 70 { 71 E_STRING eString; 72 PART_TEST part_test; 73 }; 74 75 static void InitParts(void) 76 { 77 static const PART_PAIR s_pairs[] = 78 { 79 // { eString, { eModule, id, nParts } } 80 { SH32_PROGRAMS, { shell32, 45 /* IDS_PROGRAMS "Start Menu\Programs" */, 2 } }, 81 { SH32_STARTUP, { shell32, 48 /* IDS_STARTUP "Start Menu\Programs\StartUp" */, 3 } }, 82 { SH32_STARTMENU, { shell32, 51 /* IDS_STARTMENU "Start Menu" */, 1 } }, 83 { SH32_PROGRAM_FILES, { shell32, 63 /* IDS_PROGRAM_FILES "Program Files" */, 1 } }, 84 { SH32_PROGRAM_FILES_COMMON, { shell32, 65 /* IDS_PROGRAM_FILES_COMMON "Program Files\Common Files" */, 2 } }, 85 { SH32_ADMINTOOLS, { shell32, 67 /* IDS_ADMINTOOLS "Start Menu\Programs\Administrative Tools" */, 3 } }, 86 { UENV_STARTMENU, { userenv, 11 /* IDS_STARTMENU "Start Menu" */, 1 } }, 87 { UENV_PROGRAMS, { userenv, 12 /* IDS_PROGRAMS "Start Menu\Programs" */, 2 } }, 88 { UENV_STARTUP, { userenv, 13 /* IDS_STARTUP "Start Menu\Programs\StartUp" */, 3 } }, 89 { SYSS_PROGRAMFILES, { syssetup, 3600 /* IDS_PROGRAMFILES "%SystemDrive%\Program Files" */, 2 } }, 90 { SYSS_COMMONFILES, { syssetup, 3601 /* IDS_COMMONFILES "Common Files" */, 1 } }, 91 { MMSY_STARTMENU, { mmsys, 5851 /* IDS_STARTMENU "Start Menu" */, 1 } }, 92 { EOLD_PROGRAMS, { explorer_old, 10 /* IDS_PROGRAMS "Programs" */, 1 } }, 93 }; 94 for (auto& pair : s_pairs) 95 { 96 parts.insert(std::make_pair(pair.eString, pair.part_test)); 97 } 98 } 99 100 static PART_MATCH PartMatches[] = 101 { 102 // Start Menu 103 { { SH32_PROGRAMS, 0 }, { SH32_STARTUP, 0 } }, 104 { { SH32_PROGRAMS, 0 }, { SH32_STARTMENU, 0 } }, 105 { { SH32_PROGRAMS, 0 }, { SH32_ADMINTOOLS, 0 } }, 106 { { SH32_PROGRAMS, 0 }, { UENV_STARTMENU, 0 } }, 107 { { SH32_PROGRAMS, 0 }, { UENV_PROGRAMS, 0 } }, 108 { { SH32_PROGRAMS, 0 }, { UENV_STARTUP, 0 } }, 109 { { SH32_PROGRAMS, 0 }, { MMSY_STARTMENU, 0 } }, 110 // Programs 111 { { SH32_PROGRAMS, 1 }, { SH32_STARTUP, 1 } }, 112 { { SH32_PROGRAMS, 1 }, { SH32_ADMINTOOLS, 1 } }, 113 { { SH32_PROGRAMS, 1 }, { UENV_PROGRAMS, 1 } }, 114 { { SH32_PROGRAMS, 1 }, { UENV_STARTUP, 1 } }, 115 { { SH32_PROGRAMS, 1 }, { EOLD_PROGRAMS, 0 } }, 116 // StartUp 117 { { SH32_STARTUP, 2 }, { UENV_STARTUP, 2 } }, 118 // Program Files 119 { { SH32_PROGRAM_FILES, 0 }, { SH32_PROGRAM_FILES_COMMON, 0 } }, 120 { { SH32_PROGRAM_FILES, 0 }, { SYSS_PROGRAMFILES, 1 } }, 121 // Common Files 122 { { SH32_PROGRAM_FILES_COMMON, 1 }, { SYSS_COMMONFILES, 0 } }, 123 }; 124 125 static int GetLocalisedText(_In_opt_ HINSTANCE hInstance, _In_ UINT uID, _Out_ LPWSTR lpBuffer, _In_ int cchBufferMax) 126 { 127 HRSRC hRes = FindResourceExW(hInstance, (LPWSTR)RT_STRING, 128 MAKEINTRESOURCEW((uID >> 4) + 1), curLcid); 129 130 if (!hRes) 131 hRes = FindResourceExW(hInstance, (LPWSTR)RT_STRING, 132 MAKEINTRESOURCEW((uID >> 4) + 1), 133 MAKELCID(MAKELANGID(LANG_ENGLISH, SUBLANG_DEFAULT), SORT_DEFAULT)); 134 135 if (!hRes) 136 return 0; 137 138 HGLOBAL hMem = LoadResource(hInstance, hRes); 139 if (!hMem) 140 return 0; 141 142 PWCHAR p = (PWCHAR)LockResource(hMem); 143 for (UINT i = 0; i < (uID & 0x0F); i++) p += *p + 1; 144 145 int len = (*p > cchBufferMax ? cchBufferMax : *p); 146 memcpy(lpBuffer, p + 1, len * sizeof(WCHAR)); 147 lpBuffer[len] = UNICODE_NULL; 148 return len; 149 } 150 151 static int LoadStringWrapW(_In_opt_ HINSTANCE hInstance, _In_ UINT uID, _Out_ LPWSTR lpBuffer, _In_ int cchBufferMax) 152 { 153 if (dwVersion < _WIN32_WINNT_WS03) 154 // Windows XP or lower: SetThreadLocale doesn't select user interface language 155 return GetLocalisedText(hInstance, uID, lpBuffer, cchBufferMax); 156 else 157 return LoadStringW(hInstance, uID, lpBuffer, cchBufferMax); 158 } 159 160 static DWORD CountParts(_In_ LPWSTR str) 161 { 162 DWORD count = 0; 163 LPWSTR ptr = str; 164 165 if (*ptr == UNICODE_NULL) 166 return 0; 167 168 while ((ptr = wcschr(ptr, L'\\'))) 169 { 170 count++; 171 ptr++; 172 } 173 174 return count + 1; 175 } 176 177 static LPWSTR GetPart(_In_ LPWSTR str, _In_ SIZE_T num, _Out_ SIZE_T* len) 178 { 179 DWORD count = 0; 180 LPWSTR ptr = str, next; 181 182 while (count < num && (ptr = wcschr(ptr, L'\\')) != NULL) 183 { 184 count++; 185 ptr++; 186 } 187 188 if (!ptr) 189 ptr = str; 190 191 next = wcschr(ptr, L'\\'); 192 *len = next ? next - ptr : wcslen(ptr); 193 return ptr; 194 } 195 196 static BOOL CALLBACK find_locale_id_callback( 197 _In_ HMODULE hModule, _In_ LPCWSTR type, _In_ LPCWSTR name, _In_ LANGID lang, _In_ LPARAM lParam) 198 { 199 langs.insert(lang); 200 return TRUE; 201 } 202 203 static void SetLocale(_In_ LCID lcid) 204 { 205 SetThreadLocale(lcid); 206 SetThreadUILanguage(lcid); 207 curLcid = lcid; 208 } 209 210 static void TEST_NumParts(void) 211 { 212 for (auto& p : parts) 213 { 214 E_MODULE m = p.second.eModule; 215 216 if (!mod[m]) 217 { 218 skip("No module for test %d\n", p.first); 219 continue; 220 } 221 222 WCHAR szBuffer[MAX_PATH]; 223 224 LoadStringWrapW(mod[m], p.second.id, szBuffer, _countof(szBuffer)); 225 p.second.gotParts = CountParts(szBuffer); 226 227 ok(p.second.nParts == p.second.gotParts, "Locale 0x%lX: Num parts mismatch %d - expected %lu, got %lu\n", 228 curLcid, p.first, p.second.nParts, p.second.gotParts); 229 } 230 } 231 232 static BOOL LoadPart(_In_ PART* p, _Out_ LPWSTR str, _In_ SIZE_T size) 233 { 234 auto s = parts[p->Num]; 235 E_MODULE m = s.eModule; 236 237 if (!mod[m]) 238 { 239 SetLastError(ERROR_FILE_NOT_FOUND); 240 return FALSE; 241 } 242 243 if (s.nParts != s.gotParts) 244 { 245 SetLastError(ERROR_INVALID_DATA); 246 return FALSE; 247 } 248 249 WCHAR szBuffer[MAX_PATH]; 250 LPWSTR szPart; 251 SIZE_T len; 252 253 LoadStringWrapW(mod[m], s.id, szBuffer, _countof(szBuffer)); 254 szPart = GetPart(szBuffer, p->Idx, &len); 255 StringCchCopyNW(str, size, szPart, len); 256 257 return TRUE; 258 } 259 260 static void TEST_PartMatches(void) 261 { 262 for (auto& match : PartMatches) 263 { 264 WCHAR szP1[MAX_PATH], szP2[MAX_PATH]; 265 266 if (!LoadPart(&match.p1, szP1, _countof(szP1))) 267 { 268 skip("%s for match test %d (pair 1)\n", GetLastError() == ERROR_FILE_NOT_FOUND 269 ? "No module" : "Invalid data", match.p1.Num); 270 continue; 271 } 272 273 if (!LoadPart(&match.p2, szP2, _countof(szP2))) 274 { 275 skip("%s for match test %d (pair 2)\n", GetLastError() == ERROR_FILE_NOT_FOUND 276 ? "No module" : "Invalid data", match.p2.Num); 277 continue; 278 } 279 280 ok(wcscmp(szP1, szP2) == 0, "Locale 0x%lX: Mismatching pairs %u:%u / %u:%u '%S' vs. '%S'\n", 281 curLcid, match.p1.Num, match.p1.Idx, match.p2.Num, match.p2.Idx, szP1, szP2); 282 } 283 } 284 285 static void TEST_LocaleTests(void) 286 { 287 // Initialization 288 InitParts(); 289 290 OSVERSIONINFOEXW osvi; 291 memset(&osvi, 0, sizeof(osvi)); 292 osvi.dwOSVersionInfoSize = sizeof(osvi); 293 294 GetVersionExW((LPOSVERSIONINFOW)&osvi); 295 dwVersion = (osvi.dwMajorVersion << 8) | osvi.dwMinorVersion; 296 297 WCHAR szOldDir[MAX_PATH], szBuffer[MAX_PATH]; 298 GetCurrentDirectoryW(_countof(szOldDir), szOldDir); 299 300 std::map<E_MODULE, LPCWSTR> lib; 301 #define ADD_LIB(eModule, pszPath) lib.insert(std::make_pair(eModule, pszPath)) 302 303 GetModuleFileNameW(NULL, szBuffer, _countof(szBuffer)); 304 LPCWSTR pszFind = StrStrW(szBuffer, L"modules\\rostests\\unittests"); 305 if (pszFind) 306 { 307 // We're running in ReactOS output folder 308 WCHAR szNewDir[MAX_PATH]; 309 310 StringCchCopyNW(szNewDir, _countof(szNewDir), szBuffer, pszFind - szBuffer); 311 SetCurrentDirectoryW(szNewDir); 312 313 ADD_LIB(shell32, L"dll\\win32\\shell32\\shell32.dll"); 314 ADD_LIB(userenv, L"dll\\win32\\userenv\\userenv.dll"); 315 ADD_LIB(syssetup, L"dll\\win32\\syssetup\\syssetup.dll"); 316 ADD_LIB(mmsys, L"dll\\cpl\\mmsys\\mmsys.cpl"); 317 ADD_LIB(explorer_old, L"modules\\rosapps\\applications\\explorer-old\\explorer_old.exe"); 318 } 319 else 320 { 321 ADD_LIB(shell32, L"shell32.dll"); 322 ADD_LIB(userenv, L"userenv.dll"); 323 ADD_LIB(syssetup, L"syssetup.dll"); 324 ADD_LIB(mmsys, L"mmsys.cpl"); 325 ADD_LIB(explorer_old, L"explorer_old.exe"); 326 } 327 #undef ADD_LIB 328 329 for (auto& lb : lib) 330 { 331 E_MODULE m = lb.first; 332 333 mod[m] = LoadLibraryExW(lib[m], NULL, LOAD_LIBRARY_AS_DATAFILE); 334 if (!mod[m]) 335 { 336 trace("Failed to load '%S', error %lu\n", lib[m], GetLastError()); 337 continue; 338 } 339 340 EnumResourceLanguagesW(mod[m], (LPCWSTR)RT_STRING, (LPCWSTR)LOCALE_ILANGUAGE, 341 find_locale_id_callback, NULL); 342 } 343 344 // Actual tests 345 for (auto& lang : langs) 346 { 347 SetLocale(MAKELCID(lang, SORT_DEFAULT)); 348 349 TEST_NumParts(); 350 TEST_PartMatches(); 351 } 352 353 // Perform cleanup 354 for (auto& m : mod) 355 { 356 if (!m.second) 357 continue; 358 359 FreeLibrary(m.second); 360 m.second = NULL; 361 } 362 363 SetCurrentDirectoryW(szOldDir); 364 } 365 366 START_TEST(LocaleTests) 367 { 368 TEST_LocaleTests(); 369 } 370