view src/win32/7zip/OpenArchive.cpp @ 1:f9f4f1b99eed

importing src directory
author Robert McIntyre <rlm@mit.edu>
date Sat, 03 Mar 2012 10:31:27 -0600
parents
children
line wrap: on
line source
1 #include "../stdafx.h"
2 #include "../resource.h"
3 #include <windows.h>
4 #include <mmsystem.h>
5 #include <cstdio>
6 #include <cerrno>
7 #include <cassert>
8 #include <cstring>
9 #include <map>
10 #include <vector>
11 #include <algorithm>
12 #include "7zip.h"
13 //#include "G_main.h"
14 //#include "G_dsound.h"
15 #include "OpenArchive.h"
17 LRESULT CALLBACK ArchiveFileChooser(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
18 static int s_archiveFileChooserResult = -1;
20 static HWND s_parentHWND = NULL;
21 void SetArchiveParentHWND(void* hwnd) { s_parentHWND = (HWND)hwnd; }
22 static HWND GetArchiveParentHWND() { return s_parentHWND ? s_parentHWND : AfxGetApp()->m_pMainWnd->GetSafeHwnd(); }
24 static char Str_Tmp [2048];
26 struct ArchiveFileChooserInfo
27 {
28 ArchiveFileChooserInfo(ArchiveFile& theArchive, const char** ignoreExtensions, int& numIgnoreExtensions) : archive(theArchive)
29 {
30 tryagain:
31 int numItems = archive.GetNumItems();
32 for(int i = 0; i < numItems; i++)
33 {
34 if(archive.GetItemSize(i))
35 {
36 const char* name = archive.GetItemName(i);
37 const char* ext = strrchr(name, '.');
38 bool ok = true;
39 if(ext++)
40 {
41 for(int j = 0; j < numIgnoreExtensions; j++)
42 {
43 const char* ext2 = ignoreExtensions[j];
44 const char* wild = strchr(ext2, '*');
45 if(!wild)
46 {
47 if(!_stricmp(ext, ext2))
48 {
49 ok = false;
50 break;
51 }
52 }
53 else // very limited (end only) wildcard support
54 {
55 if(!_strnicmp(ext, ext2, wild - ext2))
56 {
57 ok = false;
58 break;
59 }
60 }
61 }
62 }
63 if(ok)
64 {
65 ArchiveFileChooserInfo::FileInfo fi = { name, i };
66 files.push_back(fi);
67 }
68 }
69 }
71 if(files.empty() && numIgnoreExtensions)
72 {
73 // try again without any exclusions if we excluded everything in the archive
74 numIgnoreExtensions = 0;
75 goto tryagain;
76 }
78 // strip away prefix paths that are common to all the files
79 bool stripping = !files.empty();
80 while(stripping)
81 {
82 const char* firstName = files[0].name.c_str();
83 const char* slash = strchr(firstName, '\\');
84 const char* slash2 = strchr(firstName, '/');
85 slash = max(slash, slash2);
86 if(!slash++)
87 break;
88 for(size_t i = 1; i < files.size(); i++)
89 if(strncmp(firstName, files[i].name.c_str(), slash - firstName))
90 stripping = false;
91 if(stripping)
92 for(size_t i = 0; i < files.size(); i++)
93 files[i].name = files[i].name.substr(slash - firstName, files[i].name.length() - (slash - firstName));
94 }
96 // sort by filename
97 std::sort(files.begin(), files.end(), FileInfo::Sort);
98 }
100 //protected:
102 struct FileInfo
103 {
104 std::string name;
105 int itemIndex;
107 static bool Sort(const FileInfo& elem1, const FileInfo& elem2)
108 {
109 int comp = elem1.name.compare(elem2.name);
110 return comp ? (comp < 0) : (elem1.itemIndex < elem2.itemIndex);
111 }
112 };
114 ArchiveFile& archive;
115 std::vector<FileInfo> files;
116 };
118 static void ClearLayoutStates();
120 int ChooseItemFromArchive(ArchiveFile& archive, bool autoChooseIfOnly1, const char** ignoreExtensions, int numIgnoreExtensions)
121 {
122 int prevNumIgnoreExtensions = numIgnoreExtensions;
123 archive.m_userMadeSelection = false;
125 // prepare a list of files to choose from the archive
126 ArchiveFileChooserInfo info (archive, ignoreExtensions, numIgnoreExtensions);
128 // based on our list, decide which item in the archive to choose
130 // check if there's nothing
131 if(info.files.size() < 1)
132 {
133 MessageBox(GetArchiveParentHWND(), "The archive is either empty or encrypted.", "Nothing to load!", MB_OK | MB_ICONWARNING);
134 return -1;
135 }
137 // warn if all the files in the archive have extensions we should ignore
138 if(numIgnoreExtensions != prevNumIgnoreExtensions)
139 {
140 CString msg;
141 msg.Format("The archive appears to only contain the wrong type of files.\n\n(in \"%s\")", archive.GetArchiveFileName());
142 int answer = MessageBox(GetArchiveParentHWND(), msg, "Warning", MB_OKCANCEL | MB_ICONWARNING | MB_DEFBUTTON2);
143 if(answer == IDCANCEL)
144 return -1;
145 }
147 // if there's only 1 item, choose it
148 if(info.files.size() == 1 && autoChooseIfOnly1 && numIgnoreExtensions == prevNumIgnoreExtensions)
149 return info.files[0].itemIndex;
151 // bring up a dialog to choose the index if there's more than 1
152 DialogBoxParam(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDD_ARCHIVEFILECHOOSER), GetArchiveParentHWND(), (DLGPROC) ArchiveFileChooser,(LPARAM) &info);
153 archive.m_userMadeSelection = (s_archiveFileChooserResult != -1);
154 return s_archiveFileChooserResult;
155 }
160 #define DEFAULT_EXTENSION ".tmp"
161 #define DEFAULT_CATEGORY "vba"
163 static struct TempFiles
164 {
165 struct TemporaryFile
166 {
167 TemporaryFile(const char* cat, const char* ext)
168 {
169 if(!ext || !*ext) ext = DEFAULT_EXTENSION;
170 if(!cat || !*cat) cat = DEFAULT_CATEGORY;
171 category = cat;
173 char tempPath [2048];
174 GetTempPath(2048, tempPath);
175 //GetTempFileName(tempPath, cat, 0, filename, ext); // alas
177 char*const fname = tempPath + strlen(tempPath);
178 unsigned short start = (unsigned short)(timeGetTime() & 0xFFFF);
179 unsigned short n = start + 1;
180 while(n != start)
181 {
182 _snprintf(fname, 2048 - (fname - tempPath), "%s%04X%s", cat, n, ext);
183 FILE* file = fopen(tempPath, "wb");
184 if(file)
185 {
186 // mark the temporary file as read-only and (whatever this does) temporary
187 DWORD attributes = GetFileAttributes(tempPath);
188 attributes |= FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_TEMPORARY;
189 SetFileAttributes(tempPath, attributes);
191 fclose(file);
193 // add it to our registry of files that need to be deleted, in case we fail to terminate properly
194 TempFiles::AddEntryToGarbageRegistry(tempPath);
196 break;
197 }
198 n++;
199 }
200 strcpy(filename, tempPath);
201 }
202 TemporaryFile(const TemporaryFile& copy)
203 {
204 strcpy(filename, copy.filename);
205 category = copy.category;
206 }
207 TemporaryFile()
208 {
209 filename[0] = 0;
210 // category[0] = 0; // error
211 }
212 bool Delete(bool returnFalseOnRegistryRemovalFailure=false)
213 {
214 if(!*filename)
215 return true; // guess it already didn't exist
217 // remove read-only attribute so Windows will let us delete it
218 // (our temporary files are read-only to discourage other apps from tampering)
219 DWORD attributes = GetFileAttributes(filename);
220 if(attributes & FILE_ATTRIBUTE_READONLY)
221 SetFileAttributes(filename, attributes & ~FILE_ATTRIBUTE_READONLY);
223 if(_unlink(filename) == 0 || errno != EACCES)
224 {
225 // remove it from our registry of files that need to be deleted, to reduce accumulation
226 bool removed = TempFiles::RemoveEntryFromGarbageRegistry(filename);
228 *filename = '\0';
229 return removed || !returnFalseOnRegistryRemovalFailure; // successfully deleted or already didn't exist, return true unless registry removal failure notification was requested and that failed
230 }
232 // restore read-only if we couldn't delete it (not sure if this ever succeeds or matters though)
233 if(attributes & FILE_ATTRIBUTE_READONLY)
234 SetFileAttributes(filename, attributes);
236 return false; // failed to delete read-only or in-use file
237 }
238 char filename [MAX_PATH];
239 std::string category;
240 };
242 std::vector<TemporaryFile> tempFiles;
244 const char* GetFile(const char* category, const char* extension)
245 {
246 tempFiles.push_back(TemporaryFile(category, extension));
247 return tempFiles.back().filename;
248 }
250 void ReleaseFile(const char* filename)
251 {
252 for(int i = (int)tempFiles.size()-1; i >= 0; i--)
253 {
254 if(!strcmp(filename, tempFiles[i].filename))
255 {
256 if(tempFiles[i].Delete())
257 tempFiles.erase(tempFiles.begin() + i);
258 }
259 }
260 }
262 void ReleaseCategory(const char* cat, const char* exceptionFilename)
263 {
264 for(int i = (int)tempFiles.size()-1; i >= 0; i--)
265 {
266 if(!strcmp(cat, tempFiles[i].category.c_str()) &&
267 (!exceptionFilename ||
268 strcmp(exceptionFilename, tempFiles[i].filename)))
269 {
270 if(tempFiles[i].Delete())
271 tempFiles.erase(tempFiles.begin() + i);
272 }
273 }
274 }
276 // delete all temporary files on shutdown
277 ~TempFiles()
278 {
279 for(size_t i = 0; i < tempFiles.size(); i++)
280 {
281 tempFiles[i].Delete();
282 }
284 TempFiles::CleanOutGarbageRegistry();
285 }
287 // run this on startup to delete any files that we failed to delete last time
288 // in case we crashed or were forcefully terminated
289 TempFiles()
290 {
291 TempFiles::CleanOutGarbageRegistry();
292 }
294 static void AddEntryToGarbageRegistry(const char* filename)
295 {
296 char gbgFile[2048];
297 GetTempPath(2048, gbgFile);
298 strcat(gbgFile, "VBATempFileRecords");
299 char key[64];
300 int i = 0;
301 while(true)
302 {
303 sprintf(key, "File%d", i);
304 GetPrivateProfileString("Files", key, "", Str_Tmp, 2048, gbgFile);
305 if(!*Str_Tmp)
306 break;
307 i++;
308 }
309 WritePrivateProfileString("Files", key, filename, gbgFile);
310 }
311 static bool RemoveEntryFromGarbageRegistry(const char* filename)
312 {
313 char gbgFile[2048];
314 GetTempPath(2048, gbgFile);
315 strcat(gbgFile, "VBATempFileRecords");
316 char key[64];
317 int i = 0;
318 int deleteSlot = -1;
319 while(true)
320 {
321 sprintf(key, "File%d", i);
322 GetPrivateProfileString("Files", key, "", Str_Tmp, 2048, gbgFile);
323 if(!*Str_Tmp)
324 break;
325 if(!strcmp(Str_Tmp, filename))
326 deleteSlot = i;
327 i++;
328 }
329 --i;
330 if(i >= 0 && deleteSlot >= 0)
331 {
332 if(i != deleteSlot)
333 {
334 sprintf(key, "File%d", i);
335 GetPrivateProfileString("Files", key, "", Str_Tmp, 2048, gbgFile);
336 sprintf(key, "File%d", deleteSlot);
337 WritePrivateProfileString("Files", key, Str_Tmp, gbgFile);
338 }
339 sprintf(key, "File%d", i);
340 if(0 == WritePrivateProfileString("Files", key, NULL, gbgFile))
341 return false;
342 }
343 if(i <= 0 && deleteSlot == 0)
344 _unlink(gbgFile);
345 return true;
346 }
348 private:
349 static void CleanOutGarbageRegistry()
350 {
351 char gbgFile[2048];
352 GetTempPath(2048, gbgFile);
353 strcat(gbgFile, "VBATempFileRecords");
355 char key[64];
356 int i = 0;
357 while(true)
358 {
359 sprintf(key, "File%d", i);
360 GetPrivateProfileString("Files", key, "", Str_Tmp, 2048, gbgFile);
361 if(!*Str_Tmp)
362 break;
363 TemporaryFile temp;
364 strcpy(temp.filename, Str_Tmp);
365 if(!temp.Delete(true))
366 i++;
367 }
368 }
370 } s_tempFiles;
373 const char* GetTempFile(const char* category, const char* extension)
374 {
375 return s_tempFiles.GetFile(category, extension);
376 }
377 void ReleaseTempFile(const char* filename)
378 {
379 s_tempFiles.ReleaseFile(filename);
380 }
381 void ReleaseTempFileCategory(const char* cat, const char* exceptionFilename)
382 {
383 if(!cat || !*cat) cat = DEFAULT_CATEGORY;
384 s_tempFiles.ReleaseCategory(cat, exceptionFilename);
385 }
389 // example input Name: "C:\games.zip"
390 // example output LogicalName: "C:\games.zip|Metroid.gba"
391 // example output PhysicalName: "C:\Documents and Settings\User\Local Settings\Temp\VBA\rom7A37.gba"
392 // assumes arguments are character buffers with 2048 bytes each
393 bool ObtainFile(const char* Name, char *const & LogicalName, char *const & PhysicalName, const char* category, const char** ignoreExtensions, int numIgnoreExtensions)
394 {
395 restart:
396 char ArchivePaths [2048];
397 strcpy(LogicalName, Name);
398 strcpy(PhysicalName, Name);
399 strcpy(ArchivePaths, Name);
400 char* bar = strchr(ArchivePaths, '|');
401 if(bar)
402 {
403 PhysicalName[bar - ArchivePaths] = 0; // doesn't belong in the physical name
404 LogicalName[bar - ArchivePaths] = 0; // we'll reconstruct the logical name as we go
405 *bar++ = 0; // bar becomes the next logical archive path component
406 }
408 bool userSelected = false;
410 while(true)
411 {
412 ArchiveFile archive (PhysicalName, LogicalName);
413 if(!archive.IsCompressed())
414 {
415 if(archive.GetNumItems() > 0)
416 return true;
417 else
418 {
419 // failed or cancelled... backtrack to outermost archive if not already there
420 char* div = NULL;
421 if(LogicalName[strlen(LogicalName)-1] == '|')
422 {
423 LogicalName[strlen(LogicalName)-1] = '\0';
424 div = strrchr(LogicalName, '|');
425 }
426 if(div && userSelected)
427 goto restart;
428 else
429 return false;
430 }
431 }
432 else
433 {
434 int item = -1;
435 bool forceManual = false;
436 if(bar && *bar) // try following the in-archive part of the logical path
437 {
438 char* bar2 = strchr(bar, '|');
439 if(bar2) *bar2++ = 0;
440 int numItems = archive.GetNumItems();
441 for(int i = 0; i < numItems; i++)
442 {
443 if(archive.GetItemSize(i))
444 {
445 const char* itemName = archive.GetItemName(i);
446 if(!_stricmp(itemName, bar))
447 {
448 item = i; // match found, now we'll auto-follow the path
449 break;
450 }
451 }
452 }
453 if(item < 0)
454 {
455 forceManual = true; // we don't want it choosing something else without user permission
456 bar = NULL; // remaining archive path is invalid
457 }
458 else
459 bar = bar2; // advance to next archive path part
460 }
461 if(item < 0)
462 item = ChooseItemFromArchive(archive, !forceManual, ignoreExtensions, numIgnoreExtensions);
464 userSelected |= archive.m_userMadeSelection;
466 const char* TempFileName = s_tempFiles.GetFile(category, strrchr(archive.GetItemName(item), '.'));
467 if(!archive.ExtractItem(item, TempFileName))
468 s_tempFiles.ReleaseFile(TempFileName);
469 s_tempFiles.ReleaseFile(PhysicalName);
470 strcpy(PhysicalName, TempFileName);
471 _snprintf(LogicalName + strlen(LogicalName), 2048 - (strlen(LogicalName)+1), "|%s", archive.GetItemName(item));
472 }
473 }
474 }
478 struct ControlLayoutInfo
479 {
480 int controlID;
482 enum LayoutType // what to do when the containing window resizes
483 {
484 NONE, // leave the control where it was
485 RESIZE_END, // resize the control
486 MOVE_START, // move the control
487 };
488 LayoutType horizontalLayout;
489 LayoutType verticalLayout;
490 };
491 struct ControlLayoutState
492 {
493 int x,y,width,height;
494 bool valid;
495 ControlLayoutState() : valid(false) {}
496 };
498 static ControlLayoutInfo controlLayoutInfos [] = {
499 {IDC_LIST1, ControlLayoutInfo::RESIZE_END, ControlLayoutInfo::RESIZE_END},
500 {IDOK, ControlLayoutInfo::MOVE_START, ControlLayoutInfo::MOVE_START},
501 {ID_CANCEL, ControlLayoutInfo::MOVE_START, ControlLayoutInfo::MOVE_START},
502 };
503 static const int numControlLayoutInfos = sizeof(controlLayoutInfos)/sizeof(*controlLayoutInfos);
505 static ControlLayoutState s_layoutState [numControlLayoutInfos];
506 static int s_windowWidth = 182, s_windowHeight = 113;
509 LRESULT CALLBACK ArchiveFileChooser(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
510 {
511 RECT r, r2;
512 int dx1, dy1, dx2, dy2;
513 static std::map<int,int> s_listToItemsMap;
515 switch(uMsg)
516 {
517 case WM_INITDIALOG:
518 {
519 //Clear_Sound_Buffer();
521 //if(Full_Screen)
522 //{
523 // while (ShowCursor(false) >= 0);
524 // while (ShowCursor(true) < 0);
525 //}
527 for(int i = 0; i < numControlLayoutInfos; i++)
528 s_layoutState[i].valid = false;
530 GetWindowRect(AfxGetApp()->m_pMainWnd->GetSafeHwnd(), &r);
531 dx1 = (r.right - r.left) / 2;
532 dy1 = (r.bottom - r.top) / 2;
534 GetWindowRect(hDlg, &r2);
535 dx2 = (r2.right - r2.left) / 2;
536 dy2 = (r2.bottom - r2.top) / 2;
538 //SetWindowPos(hDlg, NULL, max(0, r.left + (dx1 - dx2)), max(0, r.top + (dy1 - dy2)), NULL, NULL, SWP_NOSIZE | SWP_NOZORDER | SWP_SHOWWINDOW);
539 SetWindowPos(hDlg, NULL, r.left, r.top, NULL, NULL, SWP_NOSIZE | SWP_NOZORDER | SWP_SHOWWINDOW);
541 ArchiveFileChooserInfo& info = *(ArchiveFileChooserInfo*)lParam;
542 std::vector<ArchiveFileChooserInfo::FileInfo>& files = info.files;
543 ArchiveFile& archive = info.archive;
545 std::string title = "Choose File in ";
546 title += archive.GetArchiveTypeName();
547 title += " Archive";
548 SetWindowText(hDlg, title.c_str());
550 // populate list
551 for(size_t i = 0; i < files.size(); i++)
552 {
553 int listIndex = SendDlgItemMessage(hDlg, IDC_LIST1, LB_ADDSTRING, (WPARAM) 0, (LONG) (LPTSTR) files[i].name.c_str());
554 s_listToItemsMap[listIndex] = files[i].itemIndex;
555 }
557 SendDlgItemMessage(hDlg, IDC_LIST1, LB_SETCURSEL, (WPARAM) 0, (LPARAM) 0);
559 {
560 RECT r3;
561 GetClientRect(hDlg, &r3);
562 s_windowWidth = r3.right - r3.left;
563 s_windowHeight = r3.bottom - r3.top;
564 }
566 return true;
567 } break;
569 case WM_SIZING:
570 {
571 // enforce a minimum window size
573 LPRECT r = (LPRECT) lParam;
574 int minimumWidth = 281;
575 int minimumHeight = 117;
576 if(r->right - r->left < minimumWidth)
577 if(wParam == WMSZ_LEFT || wParam == WMSZ_TOPLEFT || wParam == WMSZ_BOTTOMLEFT)
578 r->left = r->right - minimumWidth;
579 else
580 r->right = r->left + minimumWidth;
581 if(r->bottom - r->top < minimumHeight)
582 if(wParam == WMSZ_TOP || wParam == WMSZ_TOPLEFT || wParam == WMSZ_TOPRIGHT)
583 r->top = r->bottom - minimumHeight;
584 else
585 r->bottom = r->top + minimumHeight;
586 return TRUE;
587 }
589 case WM_SIZE:
590 {
591 // resize or move controls in the window as necessary when the window is resized
593 int prevDlgWidth = s_windowWidth;
594 int prevDlgHeight = s_windowHeight;
596 int dlgWidth = LOWORD(lParam);
597 int dlgHeight = HIWORD(lParam);
599 int deltaWidth = dlgWidth - prevDlgWidth;
600 int deltaHeight = dlgHeight - prevDlgHeight;
602 for(int i = 0; i < numControlLayoutInfos; i++)
603 {
604 ControlLayoutInfo layoutInfo = controlLayoutInfos[i];
605 ControlLayoutState& layoutState = s_layoutState[i];
607 HWND hCtrl = GetDlgItem(hDlg,layoutInfo.controlID);
609 int x,y,width,height;
610 if(layoutState.valid)
611 {
612 x = layoutState.x;
613 y = layoutState.y;
614 width = layoutState.width;
615 height = layoutState.height;
616 }
617 else
618 {
619 RECT r;
620 GetWindowRect(hCtrl, &r);
621 POINT p = {r.left, r.top};
622 ScreenToClient(hDlg, &p);
623 x = p.x;
624 y = p.y;
625 width = r.right - r.left;
626 height = r.bottom - r.top;
627 }
629 switch(layoutInfo.horizontalLayout)
630 {
631 case ControlLayoutInfo::RESIZE_END: width += deltaWidth; break;
632 case ControlLayoutInfo::MOVE_START: x += deltaWidth; break;
633 default: break;
634 }
635 switch(layoutInfo.verticalLayout)
636 {
637 case ControlLayoutInfo::RESIZE_END: height += deltaHeight; break;
638 case ControlLayoutInfo::MOVE_START: y += deltaHeight; break;
639 default: break;
640 }
642 SetWindowPos(hCtrl, 0, x,y, width,height, 0);
644 layoutState.x = x;
645 layoutState.y = y;
646 layoutState.width = width;
647 layoutState.height = height;
648 layoutState.valid = true;
649 }
651 s_windowWidth = dlgWidth;
652 s_windowHeight = dlgHeight;
654 RedrawWindow(hDlg, NULL, NULL, RDW_INVALIDATE);
655 }
656 break;
658 case WM_COMMAND:
659 switch(LOWORD(wParam))
660 {
661 case IDC_LIST1:
662 if(HIWORD(wParam) == LBN_DBLCLK)
663 {
664 POINT pos;
665 GetCursorPos(&pos);
666 int clickedItem = LBItemFromPt(GetDlgItem(hDlg, IDC_LIST1), pos, FALSE);
667 if(clickedItem != -1)
668 {
669 SendMessage(hDlg, WM_COMMAND, IDOK, 0);
670 }
671 }
672 return TRUE;
674 case IDOK:
675 {
676 int listIndex = SendDlgItemMessage(hDlg, IDC_LIST1, LB_GETCURSEL, (WPARAM) 0, (LPARAM) 0);
677 s_archiveFileChooserResult = s_listToItemsMap[listIndex];
678 s_listToItemsMap.clear();
679 //if(Full_Screen)
680 //{
681 // while (ShowCursor(true) < 0);
682 // while (ShowCursor(false) >= 0);
683 //}
684 EndDialog(hDlg, false);
685 } return TRUE;
687 case ID_CANCEL:
688 case IDCANCEL:
689 s_archiveFileChooserResult = -1;
690 s_listToItemsMap.clear();
691 //if(Full_Screen)
692 //{
693 // while (ShowCursor(true) < 0);
694 // while (ShowCursor(false) >= 0);
695 //}
696 EndDialog(hDlg, false);
697 return TRUE;
698 }
700 case WM_CLOSE:
701 s_archiveFileChooserResult = -1;
702 s_listToItemsMap.clear();
703 //if(Full_Screen)
704 //{
705 // while (ShowCursor(true) < 0);
706 // while (ShowCursor(false) >= 0);
707 //}
708 EndDialog(hDlg, false);
709 return TRUE;
710 }
712 return false;
713 }