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