1 /*
2  * PROJECT:     ReactOS api tests
3  * LICENSE:     LGPL-2.0-or-later (https://spdx.org/licenses/LGPL-2.0-or-later)
4  * PURPOSE:     Test for SHChangeNotify
5  * COPYRIGHT:   Copyright 2020 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com)
6  */
7 
8 // NOTE: This test program closes the Explorer cabinets before tests.
9 
10 #include "shelltest.h"
11 #include <shlwapi.h>
12 #include <stdio.h>
13 #include "SHChangeNotify.h"
14 
15 #define DONT_SEND 0x24242424
16 
17 static HWND s_hwnd = NULL;
18 static const WCHAR s_szName[] = L"SHChangeNotify testcase";
19 static WCHAR s_szSubProgram[MAX_PATH];
20 
21 typedef void (*ACTION)(void);
22 
23 typedef struct TEST_ENTRY
24 {
25     INT line;
26     DWORD event;
27     LPCVOID item1;
28     LPCVOID item2;
29     LPCSTR pattern;
30     ACTION action;
31     LPCWSTR path1;
32     LPCWSTR path2;
33 } TEST_ENTRY;
34 
35 static BOOL
36 DoCreateEmptyFile(LPCWSTR pszFileName)
37 {
38     FILE *fp = _wfopen(pszFileName, L"wb");
39     fclose(fp);
40     return fp != NULL;
41 }
42 
43 static void
44 DoAction1(void)
45 {
46     ok_int(CreateDirectoryW(s_dir2, NULL), TRUE);
47 }
48 
49 static void
50 DoAction2(void)
51 {
52     ok_int(RemoveDirectoryW(s_dir2), TRUE);
53 }
54 
55 static void
56 DoAction3(void)
57 {
58     ok_int(MoveFileExW(s_dir2, s_dir3, MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING), TRUE);
59 }
60 
61 static void
62 DoAction4(void)
63 {
64     ok_int(DoCreateEmptyFile(s_file1), TRUE);
65 }
66 
67 static void
68 DoAction5(void)
69 {
70     ok_int(MoveFileExW(s_file1, s_file2, MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING), TRUE);
71 }
72 
73 static void
74 DoAction6(void)
75 {
76     ok_int(DeleteFileW(s_file2), TRUE);
77 }
78 
79 static void
80 DoAction7(void)
81 {
82     DeleteFileW(s_file1);
83     DeleteFileW(s_file2);
84     ok_int(RemoveDirectoryW(s_dir3), TRUE);
85 }
86 
87 static void
88 DoAction8(void)
89 {
90     BOOL ret = RemoveDirectoryW(s_dir1);
91     ok(ret, "RemoveDirectoryW failed. GetLastError() == %ld\n", GetLastError());
92 }
93 
94 static const TEST_ENTRY s_TestEntriesMode0[] =
95 {
96     {__LINE__, SHCNE_MKDIR, s_dir2, NULL, NULL, DoAction1, NULL, NULL},
97     {__LINE__, SHCNE_RMDIR, s_dir2, NULL, "00001000", NULL, s_dir2, L""},
98     {__LINE__, SHCNE_RMDIR, s_dir2, NULL, "00001000", DoAction2, s_dir2, L""},
99     {__LINE__, SHCNE_MKDIR, s_dir2, NULL, "00010000", DoAction1, s_dir2, L""},
100     {__LINE__, SHCNE_RENAMEFOLDER, s_dir2, s_dir3, "00000001", NULL, s_dir2, s_dir3},
101     {__LINE__, SHCNE_RENAMEFOLDER, s_dir2, s_dir3, "00000001", DoAction3, s_dir2, s_dir3},
102     {__LINE__, SHCNE_CREATE, s_file1, NULL, "01000000", NULL, s_file1, L""},
103     {__LINE__, SHCNE_CREATE, s_file1, s_file2, "01000000", NULL, s_file1, s_file2},
104     {__LINE__, SHCNE_CREATE, s_file1, NULL, "01000000", DoAction4, s_file1, L""},
105     {__LINE__, SHCNE_RENAMEITEM, s_file1, s_file2, "10000000", NULL, s_file1, s_file2},
106     {__LINE__, SHCNE_RENAMEITEM, s_file1, s_file2, "10000000", DoAction5, s_file1, s_file2},
107     {__LINE__, SHCNE_RENAMEITEM, s_file1, s_file2, "10000000", NULL, s_file1, s_file2},
108     {__LINE__, SHCNE_UPDATEITEM, s_file1, NULL, "00000010", NULL, s_file1, L""},
109     {__LINE__, SHCNE_UPDATEITEM, s_file2, NULL, "00000010", NULL, s_file2, L""},
110     {__LINE__, SHCNE_DELETE, s_file1, NULL, "00100000", NULL, s_file1, L""},
111     {__LINE__, SHCNE_DELETE, s_file2, NULL, "00100000", NULL, s_file2, L""},
112     {__LINE__, SHCNE_DELETE, s_file2, NULL, "00100000", DoAction6, s_file2, L""},
113     {__LINE__, SHCNE_DELETE, s_file2, NULL, "00100000", NULL, s_file2, L""},
114     {__LINE__, SHCNE_DELETE, s_file1, NULL, "00100000", NULL, s_file1, L""},
115     {__LINE__, SHCNE_RMDIR, s_dir2, NULL, "00001000", NULL, s_dir2, L""},
116     {__LINE__, SHCNE_RMDIR, s_dir3, NULL, "00001000", DoAction7, s_dir3, L""},
117     {__LINE__, SHCNE_RMDIR, s_dir1, NULL, "00001000", NULL, s_dir1, L""},
118     {__LINE__, SHCNE_RMDIR, s_dir1, NULL, "00001000", DoAction8, s_dir1, L""},
119 };
120 
121 #define s_TestEntriesMode1 s_TestEntriesMode0
122 #define s_TestEntriesMode2 s_TestEntriesMode0
123 
124 static const TEST_ENTRY s_TestEntriesMode3[] =
125 {
126     {__LINE__, DONT_SEND, s_dir2, NULL, NULL, DoAction1, NULL, NULL},
127     {__LINE__, DONT_SEND, s_dir2, NULL, "00001000", DoAction2, s_dir2, L""},
128     {__LINE__, DONT_SEND, s_dir2, NULL, "00010000", DoAction1, s_dir2, L""},
129     {__LINE__, DONT_SEND, s_dir2, s_dir3, "00000001", DoAction3, s_dir2, s_dir3},
130     {__LINE__, DONT_SEND, s_file1, NULL, "01000000", DoAction4, s_file1, L""},
131     {__LINE__, DONT_SEND, s_file1, s_file2, "10000000", DoAction5, s_file1, s_file2},
132     {__LINE__, DONT_SEND, s_file2, NULL, "00100000", DoAction6, s_file2, L""},
133     {__LINE__, DONT_SEND, s_dir3, NULL, "00001000", DoAction7, s_dir3, L""},
134     {__LINE__, SHCNE_MKDIR, s_dir2, NULL, "00000000", NULL, NULL, NULL},
135     {__LINE__, SHCNE_INTERRUPT | SHCNE_MKDIR, s_dir2, NULL, "00000000", NULL, NULL, NULL},
136 };
137 
138 static const TEST_ENTRY s_TestEntriesMode4[] =
139 {
140     {__LINE__, DONT_SEND, s_dir2, NULL, NULL, DoAction1, NULL, NULL},
141     {__LINE__, DONT_SEND, s_dir2, NULL, "00001000", DoAction2, s_dir2, L""},
142     {__LINE__, DONT_SEND, s_dir2, NULL, "00010000", DoAction1, s_dir2, L""},
143     {__LINE__, DONT_SEND, s_dir2, s_dir3, "00000001", DoAction3, s_dir2, s_dir3},
144     {__LINE__, DONT_SEND, s_file1, NULL, "01000000", DoAction4, s_file1, L""},
145     {__LINE__, DONT_SEND, s_file1, s_file2, "10000000", DoAction5, s_file1, s_file2},
146     {__LINE__, DONT_SEND, s_file2, NULL, "00100000", DoAction6, s_file2, L""},
147     {__LINE__, DONT_SEND, s_dir3, NULL, "00001000", DoAction7, s_dir3, L""},
148     {__LINE__, SHCNE_MKDIR, s_dir2, NULL, "00000000", NULL, NULL, NULL},
149     {__LINE__, SHCNE_INTERRUPT | SHCNE_MKDIR, s_dir2, NULL, "00000000", NULL, NULL, NULL},
150 };
151 
152 static const TEST_ENTRY s_TestEntriesMode5[] =
153 {
154     {__LINE__, SHCNE_MKDIR, s_dir2, NULL, "00000000", NULL, NULL, NULL},
155     {__LINE__, SHCNE_RMDIR, s_dir2, NULL, "00000000", NULL, NULL, NULL},
156     {__LINE__, SHCNE_MKDIR, s_dir2, NULL, "00000000", DoAction1, NULL, NULL},
157     {__LINE__, SHCNE_RMDIR, s_dir2, NULL, "00000000", NULL, NULL, NULL},
158     {__LINE__, SHCNE_RMDIR, s_dir2, NULL, "00000000", DoAction2, NULL, NULL},
159     {__LINE__, SHCNE_MKDIR, s_dir2, NULL, "00000000", DoAction1, NULL, NULL},
160     {__LINE__, SHCNE_RENAMEFOLDER, s_dir2, s_dir3, "00000000", NULL, NULL, NULL},
161     {__LINE__, SHCNE_RENAMEFOLDER, s_dir2, s_dir3, "00000000", DoAction3, NULL, NULL},
162     {__LINE__, SHCNE_CREATE, s_file1, NULL, "00000000", NULL, NULL, NULL},
163     {__LINE__, SHCNE_CREATE, s_file1, s_file2, "00000000", NULL, NULL, NULL},
164     {__LINE__, SHCNE_CREATE, s_file1, NULL, "00000000", DoAction4, NULL, NULL},
165     {__LINE__, SHCNE_RENAMEITEM, s_file1, s_file2, "00000000", NULL, NULL, NULL},
166     {__LINE__, SHCNE_RENAMEITEM, s_file1, s_file2, "00000000", DoAction5, NULL, NULL},
167     {__LINE__, SHCNE_RENAMEITEM, s_file1, s_file2, "00000000", NULL, NULL, NULL},
168     {__LINE__, SHCNE_UPDATEITEM, s_file1, NULL, "00000000", NULL, NULL, NULL},
169     {__LINE__, SHCNE_UPDATEITEM, s_file2, NULL, "00000000", NULL, NULL, NULL},
170     {__LINE__, SHCNE_UPDATEITEM, s_file1, s_file2, "00000000", NULL, NULL, NULL},
171     {__LINE__, SHCNE_UPDATEITEM, s_file2, s_file1, "00000000", NULL, NULL, NULL},
172     {__LINE__, SHCNE_DELETE, s_file1, NULL, "00000000", NULL, NULL, NULL},
173     {__LINE__, SHCNE_DELETE, s_file2, NULL, "00000000", NULL, NULL, NULL},
174     {__LINE__, SHCNE_DELETE, s_file2, s_file1, "00000000", NULL, NULL, NULL},
175     {__LINE__, SHCNE_DELETE, s_file1, s_file2, "00000000", NULL, NULL, NULL},
176     {__LINE__, SHCNE_DELETE, s_file2, NULL, "00000000", DoAction6, NULL, NULL},
177     {__LINE__, SHCNE_DELETE, s_file2, NULL, "00000000", NULL, NULL, NULL},
178     {__LINE__, SHCNE_DELETE, s_file1, NULL, "00000000", NULL, NULL, NULL},
179     {__LINE__, SHCNE_INTERRUPT | SHCNE_RMDIR, s_dir1, NULL, "00000000", NULL, NULL, NULL},
180     {__LINE__, SHCNE_INTERRUPT | SHCNE_RMDIR, s_dir2, NULL, "00000000", NULL, NULL, NULL},
181     {__LINE__, SHCNE_INTERRUPT | SHCNE_RMDIR, s_dir3, NULL, "00000000", NULL, NULL, NULL},
182     {__LINE__, SHCNE_INTERRUPT | SHCNE_RMDIR, s_dir1, s_dir2, "00000000", NULL, NULL, NULL},
183     {__LINE__, SHCNE_INTERRUPT | SHCNE_RMDIR, s_dir1, s_dir3, "00000000", NULL, NULL, NULL},
184     {__LINE__, SHCNE_INTERRUPT | SHCNE_RMDIR, s_dir2, s_dir1, "00000000", NULL, NULL, NULL},
185     {__LINE__, SHCNE_INTERRUPT | SHCNE_RMDIR, s_dir2, s_dir3, "00000000", NULL, NULL, NULL},
186     {__LINE__, SHCNE_INTERRUPT | SHCNE_RMDIR, s_dir3, NULL, "00000000", NULL, NULL, NULL},
187     {__LINE__, SHCNE_INTERRUPT | SHCNE_RMDIR, s_dir3, NULL, "00000000", DoAction7, NULL, NULL},
188     {__LINE__, SHCNE_INTERRUPT | SHCNE_RMDIR, s_dir1, NULL, "00000000", NULL, NULL, NULL},
189     {__LINE__, SHCNE_INTERRUPT | SHCNE_RMDIR, s_dir1, NULL, "00000000", DoAction8, NULL, NULL},
190 };
191 
192 LPCSTR PatternFromFlags(DWORD flags)
193 {
194     static char s_buf[TYPE_RENAMEFOLDER + 1 + 1];
195     DWORD i;
196     for (i = 0; i <= TYPE_RENAMEFOLDER; ++i)
197     {
198         s_buf[i] = (char)('0' + !!(flags & (1 << i)));
199     }
200     s_buf[i] = 0;
201     return s_buf;
202 }
203 
204 static BOOL
205 DoGetPaths(LPWSTR pszPath1, LPWSTR pszPath2)
206 {
207     pszPath1[0] = pszPath2[0] = 0;
208 
209     WCHAR szText[MAX_PATH * 2];
210     szText[0] = 0;
211     if (FILE *fp = fopen(TEMP_FILE, "rb"))
212     {
213         fread(szText, 1, sizeof(szText), fp);
214         fclose(fp);
215     }
216 
217     LPWSTR pch = wcschr(szText, L'|');
218     if (pch == NULL)
219         return FALSE;
220 
221     *pch = 0;
222     lstrcpynW(pszPath1, szText, MAX_PATH);
223     lstrcpynW(pszPath2, pch + 1, MAX_PATH);
224     return TRUE;
225 }
226 
227 static void
228 DoTestEntry(const TEST_ENTRY *entry)
229 {
230     if (entry->action)
231     {
232         (*entry->action)();
233     }
234 
235     if (entry->event != DONT_SEND)
236     {
237         SHChangeNotify(entry->event, SHCNF_PATHW | SHCNF_FLUSH, entry->item1, entry->item2);
238     }
239     else
240     {
241         SHChangeNotify(0, SHCNF_FLUSH, NULL, NULL);
242     }
243 
244     DWORD flags = SendMessageW(s_hwnd, WM_GET_NOTIFY_FLAGS, 0, 0);
245     LPCSTR pattern = PatternFromFlags(flags);
246 
247     if (entry->pattern)
248     {
249         ok(lstrcmpA(pattern, entry->pattern) == 0 ||
250            lstrcmpA(pattern, "00000100") == 0, // SHCNE_UPDATEDIR
251            "Line %d: pattern mismatch '%s'\n", entry->line, pattern);
252     }
253 
254     SendMessageW(s_hwnd, WM_SET_PATHS, 0, 0);
255     Sleep(50);
256 
257     WCHAR szPath1[MAX_PATH], szPath2[MAX_PATH];
258     szPath1[0] = szPath2[0] = 0;
259     BOOL bOK = DoGetPaths(szPath1, szPath2);
260 
261     if (lstrcmpA(pattern, "00000100") == 0) // SHCNE_UPDATEDIR
262     {
263         if (entry->path1)
264             ok(bOK && lstrcmpiW(s_dir1, szPath1) == 0,
265                "Line %d: path1 mismatch '%S' (%d)\n", entry->line, szPath1, bOK);
266         if (entry->path2)
267             ok(bOK && lstrcmpiW(L"", szPath2) == 0,
268                "Line %d: path2 mismatch '%S' (%d)\n", entry->line, szPath2, bOK);
269     }
270     else
271     {
272         if (entry->path1)
273             ok(bOK && lstrcmpiW(entry->path1, szPath1) == 0,
274                "Line %d: path1 mismatch '%S' (%d)\n", entry->line, szPath1, bOK);
275         if (entry->path2)
276             ok(bOK && lstrcmpiW(entry->path2, szPath2) == 0,
277                "Line %d: path2 mismatch '%S' (%d)\n", entry->line, szPath2, bOK);
278     }
279 
280     SendMessageW(s_hwnd, WM_CLEAR_FLAGS, 0, 0);
281 }
282 
283 static BOOL
284 DoInit(void)
285 {
286     DoInitPaths();
287 
288     CreateDirectoryW(s_dir1, NULL);
289 
290     // close Explorer before tests
291     INT i, nCount = 50;
292     for (i = 0; i < nCount; ++i)
293     {
294         HWND hwnd = FindWindowW(L"CabinetWClass", NULL);
295         if (hwnd == NULL)
296             break;
297 
298         PostMessage(hwnd, WM_CLOSE, 0, 0);
299         Sleep(100);
300     }
301     if (i == nCount)
302         skip("Unable to close Explorer cabinet\n");
303 
304     return PathIsDirectoryW(s_dir1);
305 }
306 
307 static void
308 DoEnd(HWND hwnd)
309 {
310     DeleteFileW(s_file1);
311     DeleteFileW(s_file2);
312     RemoveDirectoryW(s_dir3);
313     RemoveDirectoryW(s_dir2);
314     RemoveDirectoryW(s_dir1);
315     DeleteFileA(TEMP_FILE);
316 
317     SendMessageW(s_hwnd, WM_COMMAND, IDOK, 0);
318 }
319 
320 static BOOL
321 GetSubProgramPath(void)
322 {
323     GetModuleFileNameW(NULL, s_szSubProgram, _countof(s_szSubProgram));
324     PathRemoveFileSpecW(s_szSubProgram);
325     PathAppendW(s_szSubProgram, L"shell32_apitest_sub.exe");
326 
327     if (!PathFileExistsW(s_szSubProgram))
328     {
329         PathRemoveFileSpecW(s_szSubProgram);
330         PathAppendW(s_szSubProgram, L"testdata\\shell32_apitest_sub.exe");
331 
332         if (!PathFileExistsW(s_szSubProgram))
333         {
334             return FALSE;
335         }
336     }
337 
338     return TRUE;
339 }
340 
341 static void
342 JustDoIt(INT nMode)
343 {
344     trace("nMode: %d\n", nMode);
345     SHChangeNotify(0, SHCNF_FLUSH, NULL, NULL);
346 
347     if (!DoInit())
348     {
349         skip("Unable to initialize.\n");
350         return;
351     }
352 
353     WCHAR szParams[8];
354     wsprintfW(szParams, L"%u", nMode);
355 
356     HINSTANCE hinst = ShellExecuteW(NULL, NULL, s_szSubProgram, szParams, NULL, SW_SHOWNORMAL);
357     if ((INT_PTR)hinst <= 32)
358     {
359         skip("Unable to run shell32_apitest_sub.exe.\n");
360         return;
361     }
362 
363     for (int i = 0; i < 15; ++i)
364     {
365         s_hwnd = FindWindowW(s_szName, s_szName);
366         if (s_hwnd)
367             break;
368 
369         Sleep(50);
370     }
371 
372     if (!s_hwnd)
373     {
374         skip("Unable to find window.\n");
375         return;
376     }
377 
378     switch (nMode)
379     {
380         case 0:
381         case 1:
382         case 2:
383             for (size_t i = 0; i < _countof(s_TestEntriesMode0); ++i)
384             {
385                 DoTestEntry(&s_TestEntriesMode0[i]);
386             }
387             break;
388         case 3:
389             for (size_t i = 0; i < _countof(s_TestEntriesMode3); ++i)
390             {
391                 DoTestEntry(&s_TestEntriesMode3[i]);
392             }
393             break;
394         case 4:
395             for (size_t i = 0; i < _countof(s_TestEntriesMode4); ++i)
396             {
397                 DoTestEntry(&s_TestEntriesMode4[i]);
398             }
399             break;
400         case 5:
401             for (size_t i = 0; i < _countof(s_TestEntriesMode5); ++i)
402             {
403                 DoTestEntry(&s_TestEntriesMode5[i]);
404             }
405             break;
406     }
407 
408     DoEnd(s_hwnd);
409 
410     for (int i = 0; i < 15; ++i)
411     {
412         s_hwnd = FindWindowW(s_szName, s_szName);
413         if (!s_hwnd)
414             break;
415 
416         Sleep(50);
417     }
418 }
419 
420 START_TEST(SHChangeNotify)
421 {
422     if (!GetSubProgramPath())
423     {
424         skip("shell32_apitest_sub.exe not found\n");
425     }
426 
427     JustDoIt(0);
428     JustDoIt(1);
429     JustDoIt(2);
430     JustDoIt(3);
431     JustDoIt(4);
432     JustDoIt(5);
433 }
434