1 /*
2  * PROJECT:     ReactOS Zip Shell Extension
3  * LICENSE:     GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+)
4  * PURPOSE:     Zip extraction
5  * COPYRIGHT:   Copyright 2017-2019 Mark Jansen (mark.jansen@reactos.org)
6  */
7 
8 #include "precomp.h"
9 
10 class CZipExtract :
11     public IZip
12 {
13     CStringW m_Filename;
14     CStringW m_Directory;
15     CStringA m_Password;
16     bool m_DirectoryChanged;
17     unzFile uf;
18 public:
19     CZipExtract(PCWSTR Filename)
20         :m_DirectoryChanged(false)
21         ,uf(NULL)
22     {
23         m_Filename = Filename;
24         m_Directory = m_Filename;
25         PWSTR Dir = m_Directory.GetBuffer();
26         PathRemoveExtensionW(Dir);
27         m_Directory.ReleaseBuffer();
28     }
29 
30     ~CZipExtract()
31     {
32         if (uf)
33         {
34             DPRINT1("WARNING: uf not closed!\n");
35             Close();
36         }
37     }
38 
39     void Close()
40     {
41         if (uf)
42             unzClose(uf);
43         uf = NULL;
44     }
45 
46     // *** IZip methods ***
47     STDMETHODIMP QueryInterface(REFIID riid, void  **ppvObject)
48     {
49         if (riid == IID_IUnknown)
50         {
51             *ppvObject = this;
52             AddRef();
53             return S_OK;
54         }
55         return E_NOINTERFACE;
56     }
57     STDMETHODIMP_(ULONG) AddRef(void)
58     {
59         return 2;
60     }
61     STDMETHODIMP_(ULONG) Release(void)
62     {
63         return 1;
64     }
65     STDMETHODIMP_(unzFile) getZip()
66     {
67         return uf;
68     }
69 
70     class CExtractSettingsPage : public CPropertyPageImpl<CExtractSettingsPage>
71     {
72     private:
73         HANDLE m_hExtractionThread;
74         bool m_bExtractionThreadCancel;
75 
76         CZipExtract* m_pExtract;
77         CStringA* m_pPassword;
78 
79     public:
80         CExtractSettingsPage(CZipExtract* extract, CStringA* password)
81             :CPropertyPageImpl<CExtractSettingsPage>(MAKEINTRESOURCE(IDS_WIZ_TITLE))
82             ,m_hExtractionThread(NULL)
83             ,m_bExtractionThreadCancel(false)
84             ,m_pExtract(extract)
85             ,m_pPassword(password)
86         {
87             m_psp.pszHeaderTitle = MAKEINTRESOURCE(IDS_WIZ_DEST_TITLE);
88             m_psp.pszHeaderSubTitle = MAKEINTRESOURCE(IDS_WIZ_DEST_SUBTITLE);
89             m_psp.dwFlags |= PSP_USETITLE | PSP_USEHEADERTITLE | PSP_USEHEADERSUBTITLE;
90         }
91 
92         int OnSetActive()
93         {
94             SetDlgItemTextW(IDC_DIRECTORY, m_pExtract->m_Directory);
95             m_pExtract->m_DirectoryChanged = false;
96             GetParent().CenterWindow(::GetDesktopWindow());
97             SetWizardButtons(PSWIZB_NEXT);
98             return 0;
99         }
100 
101         int OnWizardNext()
102         {
103             if (m_hExtractionThread != NULL)
104             {
105                 /* We enter here when extraction has finished, and go to next page if it succeeded */
106                 WaitForSingleObject(m_hExtractionThread, INFINITE);
107                 CloseHandle(m_hExtractionThread);
108                 m_hExtractionThread = NULL;
109                 m_pExtract->Release();
110                 if (!m_bExtractionThreadCancel)
111                 {
112                     return 0;
113                 }
114                 else
115                 {
116                     SetWindowLongPtr(DWLP_MSGRESULT, -1);
117                     return TRUE;
118                 }
119             }
120 
121             /* We end up here if the user manually clicks Next: start extraction */
122             m_bExtractionThreadCancel = false;
123 
124             /* Grey out every control during extraction to prevent user interaction */
125             ::EnableWindow(GetDlgItem(IDC_BROWSE), FALSE);
126             ::EnableWindow(GetDlgItem(IDC_DIRECTORY), FALSE);
127             ::EnableWindow(GetDlgItem(IDC_PASSWORD), FALSE);
128             SetWizardButtons(0);
129 
130             CStringW strExtracting(MAKEINTRESOURCEW(IDS_EXTRACTING));
131             SetDlgItemTextW(IDC_STATUSTEXT, strExtracting);
132 
133             if (m_pExtract->m_DirectoryChanged)
134                 UpdateDirectory();
135 
136             m_pExtract->AddRef();
137 
138             m_hExtractionThread = CreateThread(NULL, 0,
139                                                &CExtractSettingsPage::ExtractEntry,
140                                                this,
141                                                0, NULL);
142             if (!m_hExtractionThread)
143             {
144                 /* Extraction thread creation failed, do not go to the next page */
145                 DWORD err = GetLastError();
146                 DPRINT1("ERROR, m_hExtractionThread: CreateThread failed: 0x%x\n", err);
147                 m_pExtract->Release();
148 
149                 SetWindowLongPtr(DWLP_MSGRESULT, -1);
150 
151                 ::EnableWindow(GetDlgItem(IDC_BROWSE), TRUE);
152                 ::EnableWindow(GetDlgItem(IDC_DIRECTORY), TRUE);
153                 ::EnableWindow(GetDlgItem(IDC_PASSWORD), TRUE);
154                 SetWizardButtons(PSWIZB_NEXT);
155             }
156             return TRUE;
157         }
158 
159         static DWORD WINAPI ExtractEntry(LPVOID lpParam)
160         {
161             CExtractSettingsPage* pPage = (CExtractSettingsPage*)lpParam;
162             bool res = pPage->m_pExtract->Extract(pPage->m_hWnd, pPage->GetDlgItem(IDC_PROGRESS), &(pPage->m_bExtractionThreadCancel));
163             /* Failing and cancelling extraction both mean we stay on the same property page */
164             pPage->m_bExtractionThreadCancel = !res;
165 
166             pPage->SetWizardButtons(PSWIZB_NEXT);
167             if (!res)
168             {
169                 /* Extraction failed/cancelled: the page becomes interactive again */
170                 ::EnableWindow(pPage->GetDlgItem(IDC_BROWSE), TRUE);
171                 ::EnableWindow(pPage->GetDlgItem(IDC_DIRECTORY), TRUE);
172                 ::EnableWindow(pPage->GetDlgItem(IDC_PASSWORD), TRUE);
173 
174                 /* Reset the progress bar's appearance */
175                 CWindow Progress(pPage->GetDlgItem(IDC_PROGRESS));
176                 Progress.SendMessage(PBM_SETRANGE32, 0, 1);
177                 Progress.SendMessage(PBM_SETPOS, 0, 0);
178             }
179             SendMessageCallback(pPage->GetParent().m_hWnd, PSM_PRESSBUTTON, PSBTN_NEXT, 0, NULL, NULL);
180 
181             return 0;
182         }
183 
184         BOOL OnQueryCancel()
185         {
186             if (m_hExtractionThread != NULL)
187             {
188                 /* Extraction will check the value of m_bExtractionThreadCancel between each file in the archive */
189                 m_bExtractionThreadCancel = true;
190                 return TRUE;
191             }
192             return FALSE;
193         }
194 
195         struct browse_info
196         {
197             HWND hWnd;
198             LPCWSTR Directory;
199         };
200 
201         static INT CALLBACK s_BrowseCallbackProc(HWND hWnd, UINT uMsg, LPARAM lp, LPARAM pData)
202         {
203             if (uMsg == BFFM_INITIALIZED)
204             {
205                 browse_info* info = (browse_info*)pData;
206                 CWindow dlg(hWnd);
207                 dlg.SendMessage(BFFM_SETSELECTION, TRUE, (LPARAM)info->Directory);
208                 dlg.CenterWindow(info->hWnd);
209             }
210             return 0;
211         }
212 
213         LRESULT OnBrowse(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
214         {
215             BROWSEINFOW bi = { m_hWnd };
216             WCHAR path[MAX_PATH];
217             bi.pszDisplayName = path;
218             bi.lpfn = s_BrowseCallbackProc;
219             bi.ulFlags = BIF_NEWDIALOGSTYLE | BIF_RETURNFSANCESTORS | BIF_RETURNONLYFSDIRS;
220             CStringW title(MAKEINTRESOURCEW(IDS_WIZ_BROWSE_TITLE));
221             bi.lpszTitle = title;
222 
223             if (m_pExtract->m_DirectoryChanged)
224                 UpdateDirectory();
225 
226             browse_info info = { m_hWnd, m_pExtract->m_Directory.GetString() };
227             bi.lParam = (LPARAM)&info;
228 
229             CComHeapPtr<ITEMIDLIST> pidl;
230             pidl.Attach(SHBrowseForFolderW(&bi));
231 
232             WCHAR tmpPath[MAX_PATH];
233             if (pidl && SHGetPathFromIDListW(pidl, tmpPath))
234             {
235                 m_pExtract->m_Directory = tmpPath;
236                 SetDlgItemTextW(IDC_DIRECTORY, m_pExtract->m_Directory);
237                 m_pExtract->m_DirectoryChanged = false;
238             }
239             return 0;
240         }
241 
242         LRESULT OnEnChangeDirectory(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
243         {
244             m_pExtract->m_DirectoryChanged = true;
245             return 0;
246         }
247 
248         LRESULT OnPassword(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
249         {
250             CStringA Password;
251             if (_CZipAskPassword(m_hWnd, NULL, Password) == eAccept)
252             {
253                 *m_pPassword = Password;
254             }
255             return 0;
256         }
257 
258         void UpdateDirectory()
259         {
260             GetDlgItemText(IDC_DIRECTORY, m_pExtract->m_Directory);
261             m_pExtract->m_DirectoryChanged = false;
262         }
263 
264     public:
265         enum { IDD = IDD_PROPPAGEDESTINATION };
266 
267         BEGIN_MSG_MAP(CCompleteSettingsPage)
268             COMMAND_ID_HANDLER(IDC_BROWSE, OnBrowse)
269             COMMAND_ID_HANDLER(IDC_PASSWORD, OnPassword)
270             COMMAND_HANDLER(IDC_DIRECTORY, EN_CHANGE, OnEnChangeDirectory)
271             CHAIN_MSG_MAP(CPropertyPageImpl<CExtractSettingsPage>)
272         END_MSG_MAP()
273     };
274 
275 
276     class CCompleteSettingsPage : public CPropertyPageImpl<CCompleteSettingsPage>
277     {
278     private:
279         CZipExtract* m_pExtract;
280 
281     public:
282         CCompleteSettingsPage(CZipExtract* extract)
283             :CPropertyPageImpl<CCompleteSettingsPage>(MAKEINTRESOURCE(IDS_WIZ_TITLE))
284             , m_pExtract(extract)
285         {
286             m_psp.pszHeaderTitle = MAKEINTRESOURCE(IDS_WIZ_COMPL_TITLE);
287             m_psp.pszHeaderSubTitle = MAKEINTRESOURCE(IDS_WIZ_COMPL_SUBTITLE);
288             m_psp.dwFlags |= PSP_USETITLE | PSP_USEHEADERTITLE | PSP_USEHEADERSUBTITLE;
289         }
290 
291 
292         int OnSetActive()
293         {
294             SetWizardButtons(PSWIZB_FINISH);
295             CStringW Path = m_pExtract->m_Directory;
296             PWSTR Ptr = Path.GetBuffer();
297             RECT rc;
298             ::GetWindowRect(GetDlgItem(IDC_DESTDIR), &rc);
299             HDC dc = GetDC();
300             PathCompactPathW(dc, Ptr, rc.right - rc.left);
301             ReleaseDC(dc);
302             Path.ReleaseBuffer();
303             SetDlgItemTextW(IDC_DESTDIR, Path);
304             CheckDlgButton(IDC_SHOW_EXTRACTED, BST_CHECKED);
305             return 0;
306         }
307         BOOL OnWizardFinish()
308         {
309             if (IsDlgButtonChecked(IDC_SHOW_EXTRACTED) == BST_CHECKED)
310             {
311                 ShellExecuteW(NULL, L"explore", m_pExtract->m_Directory, NULL, NULL, SW_SHOW);
312             }
313             return FALSE;
314         }
315 
316     public:
317         enum { IDD = IDD_PROPPAGECOMPLETE };
318 
319         BEGIN_MSG_MAP(CCompleteSettingsPage)
320             CHAIN_MSG_MAP(CPropertyPageImpl<CCompleteSettingsPage>)
321         END_MSG_MAP()
322     };
323 
324 
325     void runWizard()
326     {
327         PROPSHEETHEADERW psh = { sizeof(psh), 0 };
328         psh.dwFlags = PSH_WIZARD97 | PSH_HEADER;
329         psh.hInstance = _AtlBaseModule.GetResourceInstance();
330 
331         CExtractSettingsPage extractPage(this, &m_Password);
332         CCompleteSettingsPage completePage(this);
333         HPROPSHEETPAGE hpsp[] =
334         {
335             extractPage.Create(),
336             completePage.Create()
337         };
338 
339         psh.phpage = hpsp;
340         psh.nPages = _countof(hpsp);
341 
342         PropertySheetW(&psh);
343     }
344 
345     bool Extract(HWND hDlg, HWND hProgress, const bool* bCancel)
346     {
347         unz_global_info64 gi;
348         uf = unzOpen2_64(m_Filename.GetString(), &g_FFunc);
349         int err = unzGetGlobalInfo64(uf, &gi);
350         if (err != UNZ_OK)
351         {
352             DPRINT1("ERROR, unzGetGlobalInfo64: 0x%x\n", err);
353             Close();
354             return false;
355         }
356 
357         CZipEnumerator zipEnum;
358         if (!zipEnum.initialize(this))
359         {
360             DPRINT1("ERROR, zipEnum.initialize\n");
361             Close();
362             return false;
363         }
364 
365         CWindow Progress(hProgress);
366         Progress.SendMessage(PBM_SETRANGE32, 0, gi.number_entry);
367         Progress.SendMessage(PBM_SETPOS, 0, 0);
368 
369         BYTE Buffer[2048];
370         CStringA BaseDirectory = m_Directory;
371         CStringA Name;
372         CStringA Password = m_Password;
373         unz_file_info64 Info;
374         int CurrentFile = 0;
375         bool bOverwriteAll = false;
376         while (zipEnum.next(Name, Info))
377         {
378             if (*bCancel)
379             {
380                 Close();
381                 return false;
382             }
383 
384             bool is_dir = Name.GetLength() > 0 && Name[Name.GetLength()-1] == '/';
385 
386             char CombinedPath[MAX_PATH * 2] = { 0 };
387             PathCombineA(CombinedPath, BaseDirectory, Name);
388             CStringA FullPath = CombinedPath;
389             FullPath.Replace('/', '\\');    /* SHPathPrepareForWriteA does not handle '/' */
390             DWORD dwFlags = SHPPFW_DIRCREATE | (is_dir ? SHPPFW_NONE : SHPPFW_IGNOREFILENAME);
391             HRESULT hr = SHPathPrepareForWriteA(hDlg, NULL, FullPath, dwFlags);
392             if (FAILED_UNEXPECTEDLY(hr))
393             {
394                 Close();
395                 return false;
396             }
397             CurrentFile++;
398             if (is_dir)
399                 continue;
400 
401             if (Info.flag & MINIZIP_PASSWORD_FLAG)
402             {
403                 eZipPasswordResponse Response = eAccept;
404                 do
405                 {
406                     /* If there is a password set, try it */
407                     if (!Password.IsEmpty())
408                     {
409                         err = unzOpenCurrentFilePassword(uf, Password);
410                         if (err == UNZ_OK)
411                         {
412                             /* Try to read some bytes, because unzOpenCurrentFilePassword does not return failure */
413                             char Buf[10];
414                             err = unzReadCurrentFile(uf, Buf, sizeof(Buf));
415                             unzCloseCurrentFile(uf);
416                             if (err >= UNZ_OK)
417                             {
418                                 /* 're'-open the file so that we can begin to extract */
419                                 err = unzOpenCurrentFilePassword(uf, Password);
420                                 break;
421                             }
422                         }
423                     }
424                     Response = _CZipAskPassword(hDlg, Name, Password);
425                 } while (Response == eAccept);
426 
427                 if (Response == eSkip)
428                 {
429                     Progress.SendMessage(PBM_SETPOS, CurrentFile, 0);
430                     continue;
431                 }
432                 else if (Response == eAbort)
433                 {
434                     Close();
435                     return false;
436                 }
437             }
438             else
439             {
440                 err = unzOpenCurrentFile(uf);
441             }
442 
443             if (err != UNZ_OK)
444             {
445                 DPRINT1("ERROR, unzOpenCurrentFilePassword: 0x%x\n", err);
446                 Close();
447                 return false;
448             }
449 
450             HANDLE hFile = CreateFileA(FullPath, GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
451             if (hFile == INVALID_HANDLE_VALUE)
452             {
453                 DWORD dwErr = GetLastError();
454                 if (dwErr == ERROR_FILE_EXISTS)
455                 {
456                     bool bOverwrite = bOverwriteAll;
457                     if (!bOverwriteAll)
458                     {
459                         eZipConfirmResponse Result = _CZipAskReplace(hDlg, FullPath);
460                         switch (Result)
461                         {
462                         case eYesToAll:
463                             bOverwriteAll = true;
464                         case eYes:
465                             bOverwrite = true;
466                             break;
467                         case eNo:
468                             break;
469                         case eCancel:
470                             unzCloseCurrentFile(uf);
471                             Close();
472                             return false;
473                         }
474                     }
475 
476                     if (bOverwrite)
477                     {
478                         hFile = CreateFileA(FullPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
479                         if (hFile == INVALID_HANDLE_VALUE)
480                         {
481                             dwErr = GetLastError();
482                         }
483                     }
484                     else
485                     {
486                         unzCloseCurrentFile(uf);
487                         continue;
488                     }
489                 }
490                 if (hFile == INVALID_HANDLE_VALUE)
491                 {
492                     unzCloseCurrentFile(uf);
493                     DPRINT1("ERROR, CreateFileA: 0x%x (%s)\n", dwErr, bOverwriteAll ? "Y" : "N");
494                     Close();
495                     return false;
496                 }
497             }
498 
499             do
500             {
501                 if (*bCancel)
502                 {
503                     CloseHandle(hFile);
504                     BOOL deleteResult = DeleteFileA(FullPath);
505                     if (deleteResult == 0)
506                         DPRINT1("ERROR, DeleteFileA: 0x%x\n", GetLastError());
507                     Close();
508                     return false;
509                 }
510 
511                 err = unzReadCurrentFile(uf, Buffer, sizeof(Buffer));
512 
513                 if (err < 0)
514                 {
515                     DPRINT1("ERROR, unzReadCurrentFile: 0x%x\n", err);
516                     break;
517                 }
518                 else if (err > 0)
519                 {
520                     DWORD dwWritten;
521                     if (!WriteFile(hFile, Buffer, err, &dwWritten, NULL))
522                     {
523                         DPRINT1("ERROR, WriteFile: 0x%x\n", GetLastError());
524                         break;
525                     }
526                     if (dwWritten != (DWORD)err)
527                     {
528                         DPRINT1("ERROR, WriteFile: dwWritten:%d err:%d\n", dwWritten, err);
529                         break;
530                     }
531                 }
532 
533             } while (err > 0);
534 
535             /* Update Filetime */
536             FILETIME LocalFileTime;
537             DosDateTimeToFileTime((WORD)(Info.dosDate >> 16), (WORD)Info.dosDate, &LocalFileTime);
538             FILETIME FileTime;
539             LocalFileTimeToFileTime(&LocalFileTime, &FileTime);
540             SetFileTime(hFile, &FileTime, &FileTime, &FileTime);
541 
542             /* Done */
543             CloseHandle(hFile);
544 
545             if (err)
546             {
547                 unzCloseCurrentFile(uf);
548                 DPRINT1("ERROR, unzReadCurrentFile2: 0x%x\n", err);
549                 Close();
550                 return false;
551             }
552             else
553             {
554                 err = unzCloseCurrentFile(uf);
555                 if (err != UNZ_OK)
556                 {
557                     DPRINT1("ERROR(non-fatal), unzCloseCurrentFile: 0x%x\n", err);
558                 }
559             }
560             Progress.SendMessage(PBM_SETPOS, CurrentFile, 0);
561         }
562 
563         Close();
564         return true;
565     }
566 };
567 
568 
569 void _CZipExtract_runWizard(PCWSTR Filename)
570 {
571     CZipExtract extractor(Filename);
572     extractor.runWizard();
573 }
574 
575