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
InitParts(void)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
GetLocalisedText(_In_opt_ HINSTANCE hInstance,_In_ UINT uID,_Out_ LPWSTR lpBuffer,_In_ int cchBufferMax)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
LoadStringWrapW(_In_opt_ HINSTANCE hInstance,_In_ UINT uID,_Out_ LPWSTR lpBuffer,_In_ int cchBufferMax)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
CountParts(_In_ LPWSTR str)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
GetPart(_In_ LPWSTR str,_In_ SIZE_T num,_Out_ SIZE_T * len)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
find_locale_id_callback(_In_ HMODULE hModule,_In_ LPCWSTR type,_In_ LPCWSTR name,_In_ LANGID lang,_In_ LPARAM lParam)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
SetLocale(_In_ LCID lcid)203 static void SetLocale(_In_ LCID lcid)
204 {
205 SetThreadLocale(lcid);
206 SetThreadUILanguage(lcid);
207 curLcid = lcid;
208 }
209
TEST_NumParts(void)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
LoadPart(_In_ PART * p,_Out_ LPWSTR str,_In_ SIZE_T size)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
TEST_PartMatches(void)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
TEST_LocaleTests(void)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
START_TEST(LocaleTests)366 START_TEST(LocaleTests)
367 {
368 TEST_LocaleTests();
369 }
370