// // Copyright (C) Anders Kjersem. Licensed under the zlib/libpng license // #include "InetBgDL.h" #define USERAGENT _T("NSIS InetBgDL (Mozilla)") #define STATUS_COMPLETEDALL 0 #define STATUS_INITIAL 202 #define STATUS_CONNECTING STATUS_INITIAL //102 #define STATUS_DOWNLOADING STATUS_INITIAL #define STATUS_ERR_GETLASTERROR 418 //HTTP: I'm a teapot: Win32 error code in $3 #define STATUS_ERR_LOCALFILEWRITEERROR 450 //HTTP: MS parental control extension #define STATUS_ERR_CANCELLED 499 typedef DWORD FILESIZE_T; // Limit to 4GB for now... #define FILESIZE_UNKNOWN (-1) #define MAX_STRLEN 1024 HINSTANCE g_hInst; NSIS::stack_t*g_pLocations = NULL; HANDLE g_hThread = NULL; HANDLE g_hGETStartedEvent = NULL; volatile UINT g_FilesTotal = 0; volatile UINT g_FilesCompleted = 0; volatile UINT g_Status = STATUS_INITIAL; volatile FILESIZE_T g_cbCurrXF; volatile FILESIZE_T g_cbCurrTot = FILESIZE_UNKNOWN; CRITICAL_SECTION g_CritLock; UINT g_N_CCH; PTSTR g_N_Vars; TCHAR g_ServerIP[128] = { _T('\0') }; DWORD g_ConnectTimeout = 0; DWORD g_ReceiveTimeout = 0; #define NSISPI_INITGLOBALS(N_CCH, N_Vars) do { \ g_N_CCH = N_CCH; \ g_N_Vars = N_Vars; \ } while(0) #define ONELOCKTORULETHEMALL #ifdef ONELOCKTORULETHEMALL #define TaskLock_AcquireExclusive() EnterCriticalSection(&g_CritLock) #define TaskLock_ReleaseExclusive() LeaveCriticalSection(&g_CritLock) #define StatsLock_AcquireExclusive() TaskLock_AcquireExclusive() #define StatsLock_ReleaseExclusive() TaskLock_ReleaseExclusive() #define StatsLock_AcquireShared() StatsLock_AcquireExclusive() #define StatsLock_ReleaseShared() StatsLock_ReleaseExclusive() #endif PTSTR NSIS_SetRegStr(UINT Reg, LPCTSTR Value) { PTSTR s = g_N_Vars + (Reg * g_N_CCH); lstrcpy(s, Value); return s; } #define NSIS_SetRegStrEmpty(r) NSIS_SetRegStr(r, _T("")) void NSIS_SetRegUINT(UINT Reg, UINT Value) { TCHAR buf[32]; wsprintf(buf, _T("%u"), Value); NSIS_SetRegStr(Reg, buf); } #define StackFreeItem(pI) GlobalFree(pI) NSIS::stack_t* StackPopItem(NSIS::stack_t**ppST) { if (*ppST) { NSIS::stack_t*pItem = *ppST; *ppST = pItem->next; return pItem; } return NULL; } void Reset() { // The g_hGETStartedEvent event is used to make sure that the Get() call will // acquire the lock before the Reset() call acquires the lock. if (g_hGETStartedEvent) { TRACE(_T("InetBgDl: waiting on g_hGETStartedEvent\n")); WaitForSingleObject(g_hGETStartedEvent, INFINITE); CloseHandle(g_hGETStartedEvent); g_hGETStartedEvent = NULL; } TaskLock_AcquireExclusive(); #ifndef ONELOCKTORULETHEMALL StatsLock_AcquireExclusive(); #endif g_FilesTotal = 0; // This causes the Task thread to exit the transfer loop if (g_hThread) { TRACE(_T("InetBgDl: waiting on g_hThread\n")); if (WAIT_OBJECT_0 != WaitForSingleObject(g_hThread, 10 * 1000)) { TRACE(_T("InetBgDl: terminating g_hThread\n")); TerminateThread(g_hThread, ERROR_OPERATION_ABORTED); } CloseHandle(g_hThread); g_hThread = NULL; } g_FilesTotal = 0; g_FilesCompleted = 0; g_Status = STATUS_INITIAL; #ifndef ONELOCKTORULETHEMALL StatsLock_ReleaseExclusive(); #endif for (NSIS::stack_t*pTmpTast,*pTask = g_pLocations; pTask ;) { pTmpTast = pTask; pTask = pTask->next; StackFreeItem(pTmpTast); } g_pLocations = NULL; TaskLock_ReleaseExclusive(); } UINT_PTR __cdecl NSISPluginCallback(UINT Event) { switch(Event) { case NSPIM_UNLOAD: Reset(); break; } return NULL; } void __stdcall InetStatusCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength) { if (dwInternetStatus == INTERNET_STATUS_NAME_RESOLVED) { // The documentation states the IP address is a PCTSTR but it is usually a // PCSTR and only sometimes a PCTSTR. StatsLock_AcquireExclusive(); wsprintf(g_ServerIP, _T("%S"), lpvStatusInformation); if (wcslen(g_ServerIP) == 1) { wsprintf(g_ServerIP, _T("%s"), lpvStatusInformation); } StatsLock_ReleaseExclusive(); } #if defined(PLUGIN_DEBUG) switch (dwInternetStatus) { case INTERNET_STATUS_RESOLVING_NAME: TRACE(_T("InetBgDl: INTERNET_STATUS_RESOLVING_NAME (%d), name=%s\n"), dwStatusInformationLength, lpvStatusInformation); break; case INTERNET_STATUS_NAME_RESOLVED: TRACE(_T("InetBgDl: INTERNET_STATUS_NAME_RESOLVED (%d), resolved name=%s\n"), dwStatusInformationLength, g_ServerIP); break; case INTERNET_STATUS_CONNECTING_TO_SERVER: TRACE(_T("InetBgDl: INTERNET_STATUS_CONNECTING_TO_SERVER (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_CONNECTED_TO_SERVER: TRACE(_T("InetBgDl: INTERNET_STATUS_CONNECTED_TO_SERVER (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_SENDING_REQUEST: TRACE(_T("InetBgDl: INTERNET_STATUS_SENDING_REQUEST (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_REQUEST_SENT: TRACE(_T("InetBgDl: INTERNET_STATUS_REQUEST_SENT (%d), bytes sent=%d\n"), dwStatusInformationLength, lpvStatusInformation); break; case INTERNET_STATUS_RECEIVING_RESPONSE: TRACE(_T("InetBgDl: INTERNET_STATUS_RECEIVING_RESPONSE (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_RESPONSE_RECEIVED: TRACE(_T("InetBgDl: INTERNET_STATUS_RESPONSE_RECEIVED (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_CTL_RESPONSE_RECEIVED: TRACE(_T("InetBgDl: INTERNET_STATUS_CTL_RESPONSE_RECEIVED (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_PREFETCH: TRACE(_T("InetBgDl: INTERNET_STATUS_PREFETCH (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_CLOSING_CONNECTION: TRACE(_T("InetBgDl: INTERNET_STATUS_CLOSING_CONNECTION (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_CONNECTION_CLOSED: TRACE(_T("InetBgDl: INTERNET_STATUS_CONNECTION_CLOSED (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_HANDLE_CREATED: TRACE(_T("InetBgDl: INTERNET_STATUS_HANDLE_CREATED (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_HANDLE_CLOSING: TRACE(_T("InetBgDl: INTERNET_STATUS_HANDLE_CLOSING (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_DETECTING_PROXY: TRACE(_T("InetBgDl: INTERNET_STATUS_DETECTING_PROXY (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_REQUEST_COMPLETE: TRACE(_T("InetBgDl: INTERNET_STATUS_REQUEST_COMPLETE (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_REDIRECT: TRACE(_T("InetBgDl: INTERNET_STATUS_REDIRECT (%d), new url=%s\n"), dwStatusInformationLength, lpvStatusInformation); break; case INTERNET_STATUS_INTERMEDIATE_RESPONSE: TRACE(_T("InetBgDl: INTERNET_STATUS_INTERMEDIATE_RESPONSE (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_USER_INPUT_REQUIRED: TRACE(_T("InetBgDl: INTERNET_STATUS_USER_INPUT_REQUIRED (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_STATE_CHANGE: TRACE(_T("InetBgDl: INTERNET_STATUS_STATE_CHANGE (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_COOKIE_SENT: TRACE(_T("InetBgDl: INTERNET_STATUS_COOKIE_SENT (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_COOKIE_RECEIVED: TRACE(_T("InetBgDl: INTERNET_STATUS_COOKIE_RECEIVED (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_PRIVACY_IMPACTED: TRACE(_T("InetBgDl: INTERNET_STATUS_PRIVACY_IMPACTED (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_P3P_HEADER: TRACE(_T("InetBgDl: INTERNET_STATUS_P3P_HEADER (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_P3P_POLICYREF: TRACE(_T("InetBgDl: INTERNET_STATUS_P3P_POLICYREF (%d)\n"), dwStatusInformationLength); break; case INTERNET_STATUS_COOKIE_HISTORY: TRACE(_T("InetBgDl: INTERNET_STATUS_COOKIE_HISTORY (%d)\n"), dwStatusInformationLength); break; default: TRACE(_T("InetBgDl: Unknown Status %d\n"), dwInternetStatus); break; } #endif } DWORD CALLBACK TaskThreadProc(LPVOID ThreadParam) { NSIS::stack_t *pURL,*pFile; HINTERNET hInetSes = NULL, hInetFile = NULL; DWORD cbio = sizeof(DWORD); HANDLE hLocalFile; bool completedFile = false; startnexttask: hLocalFile = INVALID_HANDLE_VALUE; pFile = NULL; TaskLock_AcquireExclusive(); // Now that we've acquired the lock, we can set the event to indicate this. // SetEvent will likely never fail, but if it does we should set it to NULL // to avoid anyone waiting on it. if (!SetEvent(g_hGETStartedEvent)) { CloseHandle(g_hGETStartedEvent); g_hGETStartedEvent = NULL; } pURL = g_pLocations; if (pURL) { pFile = pURL->next; g_pLocations = pFile->next; } #ifndef ONELOCKTORULETHEMALL StatsLock_AcquireExclusive(); #endif if (completedFile) { ++g_FilesCompleted; } completedFile = false; g_cbCurrXF = 0; g_cbCurrTot = FILESIZE_UNKNOWN; if (!pURL) { if (g_FilesTotal) { if (g_FilesTotal == g_FilesCompleted) { g_Status = STATUS_COMPLETEDALL; } } g_hThread = NULL; } #ifndef ONELOCKTORULETHEMALL StatsLock_ReleaseExclusive(); #endif TaskLock_ReleaseExclusive(); if (!pURL) { if (0) { diegle: DWORD gle = GetLastError(); //TODO? if (ERROR_INTERNET_EXTENDED_ERROR==gle) InternetGetLastResponseInfo(...) g_Status = STATUS_ERR_GETLASTERROR; } if (hInetSes) { InternetCloseHandle(hInetSes); } if (INVALID_HANDLE_VALUE != hLocalFile) { CloseHandle(hLocalFile); } StackFreeItem(pURL); StackFreeItem(pFile); return 0; } if (!hInetSes) { hInetSes = InternetOpen(USERAGENT, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if (!hInetSes) { TRACE(_T("InetBgDl: InternetOpen failed with gle=%u\n"), GetLastError()); goto diegle; } InternetSetStatusCallback(hInetSes, (INTERNET_STATUS_CALLBACK)InetStatusCallback); //msdn.microsoft.com/library/default.asp?url=/workshop/components/offline/offline.asp#Supporting Offline Browsing in Applications and Components ULONG longOpt; DWORD cbio = sizeof(ULONG); if (InternetQueryOption(hInetSes, INTERNET_OPTION_CONNECTED_STATE, &longOpt, &cbio)) { if (INTERNET_STATE_DISCONNECTED_BY_USER&longOpt) { INTERNET_CONNECTED_INFO ci = {INTERNET_STATE_CONNECTED, 0}; InternetSetOption(hInetSes, INTERNET_OPTION_CONNECTED_STATE, &ci, sizeof(ci)); } } // Change the default connect timeout if specified. if(g_ConnectTimeout > 0) { InternetSetOption(hInetSes, INTERNET_OPTION_CONNECT_TIMEOUT, &g_ConnectTimeout, sizeof(g_ConnectTimeout)); } // Change the default receive timeout if specified. if (g_ReceiveTimeout) { InternetSetOption(hInetSes, INTERNET_OPTION_RECEIVE_TIMEOUT, &g_ReceiveTimeout, sizeof(DWORD)); } } DWORD ec = ERROR_SUCCESS; hLocalFile = CreateFile(pFile->text, GENERIC_WRITE,FILE_SHARE_READ | FILE_SHARE_DELETE,NULL,CREATE_ALWAYS, 0, NULL); if (INVALID_HANDLE_VALUE == hLocalFile) { TRACE(_T("InetBgDl: CreateFile file handle invalid\n")); goto diegle; } const DWORD IOURedirFlags = INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP | INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS; const DWORD IOUCacheFlags = INTERNET_FLAG_RESYNCHRONIZE | INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_PRAGMA_NOCACHE | INTERNET_FLAG_RELOAD; const DWORD IOUCookieFlags = INTERNET_FLAG_NO_COOKIES; DWORD IOUFlags = IOURedirFlags | IOUCacheFlags | IOUCookieFlags | INTERNET_FLAG_NO_UI | INTERNET_FLAG_EXISTING_CONNECT; TCHAR *hostname = (TCHAR*) GlobalAlloc(GPTR, MAX_STRLEN * sizeof(TCHAR)), *urlpath = (TCHAR*) GlobalAlloc(GPTR, MAX_STRLEN * sizeof(TCHAR)), *extrainfo = (TCHAR*) GlobalAlloc(GPTR, MAX_STRLEN * sizeof(TCHAR)); URL_COMPONENTS uc = { sizeof(URL_COMPONENTS), NULL, 0, (INTERNET_SCHEME)0, hostname, MAX_STRLEN, (INTERNET_PORT)0, NULL, 0, NULL, 0, urlpath, MAX_STRLEN, extrainfo, MAX_STRLEN}; uc.dwHostNameLength = uc.dwUrlPathLength = uc.dwExtraInfoLength = MAX_STRLEN; if (!InternetCrackUrl(pURL->text, 0, ICU_ESCAPE, &uc)) { // Bad url or param passed in TRACE(_T("InetBgDl: InternetCrackUrl false with url=%s, gle=%u\n"), pURL->text, GetLastError()); goto diegle; } TRACE(_T("InetBgDl: scheme_id=%d, hostname=%s, port=%d, urlpath=%s, extrainfo=%s\n"), uc.nScheme, hostname, uc.nPort, urlpath, extrainfo); // Only http and https are supported if (uc.nScheme != INTERNET_SCHEME_HTTP && uc.nScheme != INTERNET_SCHEME_HTTPS) { TRACE(_T("InetBgDl: only http and https is supported, aborting...\n")); goto diegle; } TRACE(_T("InetBgDl: calling InternetOpenUrl with url=%s\n"), pURL->text); hInetFile = InternetOpenUrl(hInetSes, pURL->text, NULL, 0, IOUFlags | (uc.nScheme == INTERNET_SCHEME_HTTPS ? INTERNET_FLAG_SECURE : 0), 1); if (!hInetFile) { TRACE(_T("InetBgDl: InternetOpenUrl failed with gle=%u\n"), GetLastError()); goto diegle; } // Get the file length via the Content-Length header FILESIZE_T cbThisFile; cbio = sizeof(cbThisFile); if (!HttpQueryInfo(hInetFile, HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER, &cbThisFile, &cbio, NULL)) { cbThisFile = FILESIZE_UNKNOWN; } TRACE(_T("InetBgDl: file size=%d bytes\n"), cbThisFile); // Setup a buffer of size 256KiB to store the downloaded data. const UINT cbBufXF = 262144; // Use a 4MiB read buffer for the connection. // Bigger buffers will be faster. // cbReadBufXF should be a multiple of cbBufXF. const UINT cbReadBufXF = 4194304; BYTE bufXF[cbBufXF]; // Up the default internal buffer size from 4096 to internalReadBufferSize. DWORD internalReadBufferSize = cbReadBufXF; if (!InternetSetOption(hInetFile, INTERNET_OPTION_READ_BUFFER_SIZE, &internalReadBufferSize, sizeof(DWORD))) { TRACE(_T("InetBgDl: InternetSetOption failed to set read buffer size to %u bytes, gle=%u\n"), internalReadBufferSize, GetLastError()); // Maybe it's too big, try half of the optimal value. If that fails just // use the default. internalReadBufferSize /= 2; if (!InternetSetOption(hInetFile, INTERNET_OPTION_READ_BUFFER_SIZE, &internalReadBufferSize, sizeof(DWORD))) { TRACE(_T("InetBgDl: InternetSetOption failed to set read buffer size ") \ _T("to %u bytes (using default read buffer size), gle=%u\n"), internalReadBufferSize, GetLastError()); } } for(;;) { DWORD cbio = 0, cbXF = 0; BOOL retXF = InternetReadFile(hInetFile, bufXF, cbBufXF, &cbio); if (!retXF) { ec = GetLastError(); TRACE(_T("InetBgDl: InternetReadFile failed, gle=%u\n"), ec); break; } if (0 == cbio) { ASSERT(ERROR_SUCCESS == ec); // EOF or broken connection? // TODO: Can InternetQueryDataAvailable detect this? TRACE(_T("InetBgDl: InternetReadFile true with 0 cbio, cbThisFile=%d, gle=%u\n"), cbThisFile, GetLastError()); // If we haven't transferred all of the file, and we know how big the file // is, and we have no more data to read from the HTTP request, then set a // broken pipe error. Reading without StatsLock is ok in this thread. if (FILESIZE_UNKNOWN != cbThisFile && g_cbCurrXF != cbThisFile) { TRACE(_T("InetBgDl: downloaded file size of %d bytes doesn't equal ") \ _T("expected file size of %d bytes\n"), g_cbCurrXF, cbThisFile); ec = ERROR_BROKEN_PIPE; } break; } // Check if we canceled the download if (0 == g_FilesTotal) { TRACE(_T("InetBgDl: 0 == g_FilesTotal, aborting transfer loop...\n")); ec = ERROR_CANCELLED; break; } cbXF = cbio; if (cbXF) { retXF = WriteFile(hLocalFile, bufXF, cbXF, &cbio, NULL); if (!retXF || cbXF != cbio) { ec = GetLastError(); break; } StatsLock_AcquireExclusive(); if (FILESIZE_UNKNOWN != cbThisFile) { g_cbCurrTot = cbThisFile; } g_cbCurrXF += cbXF; StatsLock_ReleaseExclusive(); } } TRACE(_T("InetBgDl: TaskThreadProc completed %s, ec=%u\n"), pURL->text, ec); InternetCloseHandle(hInetFile); if (ERROR_SUCCESS == ec) { if (INVALID_HANDLE_VALUE != hLocalFile) { CloseHandle(hLocalFile); hLocalFile = INVALID_HANDLE_VALUE; } StackFreeItem(pURL); StackFreeItem(pFile); ++completedFile; } else { TRACE(_T("InetBgDl: failed with ec=%u\n"), ec); SetLastError(ec); goto diegle; } goto startnexttask; } NSISPIEXPORTFUNC Get(HWND hwndNSIS, UINT N_CCH, TCHAR*N_Vars, NSIS::stack_t**ppST, NSIS::xparams_t*pX) { pX->RegisterPluginCallback(g_hInst, NSISPluginCallback); for (;;) { NSIS::stack_t*pURL = StackPopItem(ppST); if (!pURL) { break; } if (lstrcmpi(pURL->text, _T("/connecttimeout")) == 0) { NSIS::stack_t*pConnectTimeout = StackPopItem(ppST); g_ConnectTimeout = _tcstol(pConnectTimeout->text, NULL, 10) * 1000; continue; } else if (lstrcmpi(pURL->text, _T("/receivetimeout")) == 0) { NSIS::stack_t*pReceiveTimeout = StackPopItem(ppST); g_ReceiveTimeout = _tcstol(pReceiveTimeout->text, NULL, 10) * 1000; continue; } else if (lstrcmpi(pURL->text, _T("/reset")) == 0) { StackFreeItem(pURL); Reset(); continue; } else if (lstrcmpi(pURL->text, _T("/end")) == 0) { freeurlandexit: StackFreeItem(pURL); break; } NSIS::stack_t*pFile = StackPopItem(ppST); if (!pFile) { goto freeurlandexit; } TaskLock_AcquireExclusive(); pFile->next = NULL; pURL->next = pFile; NSIS::stack_t*pTasksTail = g_pLocations; while(pTasksTail && pTasksTail->next) pTasksTail = pTasksTail->next; if (pTasksTail) { pTasksTail->next = pURL; } else { g_pLocations = pURL; } if (!g_hThread) { DWORD tid; if (g_hGETStartedEvent) { CloseHandle(g_hGETStartedEvent); } g_hGETStartedEvent = CreateEvent(NULL, TRUE, FALSE, NULL); g_hThread = CreateThread(NULL, 0, TaskThreadProc, NULL, 0, &tid); } if (!g_hThread) { goto freeurlandexit; } #ifndef ONELOCKTORULETHEMALL StatsLock_AcquireExclusive(); #endif ++g_FilesTotal; #ifndef ONELOCKTORULETHEMALL StatsLock_ReleaseExclusive(); #endif TaskLock_ReleaseExclusive(); } } NSISPIEXPORTFUNC GetStats(HWND hwndNSIS, UINT N_CCH, TCHAR*N_Vars, NSIS::stack_t**ppST, NSIS::xparams_t*pX) { NSISPI_INITGLOBALS(N_CCH, N_Vars); StatsLock_AcquireShared(); NSIS_SetRegUINT(0, g_Status); NSIS_SetRegUINT(1, g_FilesCompleted); NSIS_SetRegUINT(2, g_FilesTotal - g_FilesCompleted); NSIS_SetRegUINT(3, g_cbCurrXF); NSIS_SetRegStrEmpty(4); if (FILESIZE_UNKNOWN != g_cbCurrTot) { NSIS_SetRegUINT(4, g_cbCurrTot); } NSIS_SetRegStr(5, g_ServerIP); StatsLock_ReleaseShared(); } EXTERN_C BOOL WINAPI _DllMainCRTStartup(HMODULE hInst, UINT Reason, LPVOID pCtx) { if (DLL_PROCESS_ATTACH==Reason) { g_hInst=hInst; InitializeCriticalSection(&g_CritLock); } return TRUE; } BOOL WINAPI DllMain(HINSTANCE hInst, ULONG Reason, LPVOID pCtx) { return _DllMainCRTStartup(hInst, Reason, pCtx); } // For some reason VC6++ doesn't like wcsicmp and swprintf. // If you use them, you get a linking error about _main // as an unresolved external. int main(int argc, char**argv) { return 0; }