|
|
/*++
Microsoft Windows Copyright (C) Microsoft Corporation, 1981 - 1999
Module Name:
utils.cpp
Abstract:
Author:
Rahul Thombre (RahulTh) 4/30/1998
Revision History:
4/30/1998 RahulTh
Created this module.
10/12/98 RahulTh
Better error handling capabilities added : CError etc.
--*/
#include "precomp.hxx"
#include <irmonftp.h>
LONG g_lLinkOnDesktop = 0;
//the path of the desktop folder.
TCHAR g_lpszDesktopFolder[MAX_PATH]; //the path to the send to folder;
TCHAR g_lpszSendToFolder[MAX_PATH];
BOOL GetShortcutInfo ( LPTSTR lpszShortcutName, ULONG ShortCutNameLength, LPTSTR lpszFullExeName, ULONG ExeNameLength );
BOOL GetSendToInfo ( LPTSTR lpszSendToName, ULONG SendToNameLength, LPTSTR lpszFullExeName, ULONG FullExeNameLength );
#define VALIDATE_SEND_COOKIE(cookie) \
{ \ __try \ { \ *pStatus = ERROR_INVALID_DATA; \ if (MAGIC_ID != ((CSendProgress *)cookie)->m_dwMagicID) \ return; \ *pStatus = ERROR_SUCCESS; \ } \ __except (EXCEPTION_EXECUTE_HANDLER) \ { \ return; \ } \ } //
// wireless link specific errors
//
ERROR_TO_STRING_ID g_ErrorToStringId[] = { {ERROR_IRTRANP_OUT_OF_MEMORY, IDS_ERROR_NO_MEMORY}, {ERROR_IRTRANP_DISK_FULL, IDS_ERROR_DISK_FULL}, {ERROR_SCEP_CANT_CREATE_FILE, IDS_ERROR_DISK_FULL}, {ERROR_SCEP_ABORT, IDS_ERROR_ABORTED}, {ERROR_SCEP_INVALID_PROTOCOL, IDS_ERROR_PROTOCOL}, {ERROR_SCEP_PDU_TOO_LARGE, IDS_ERROR_PROTOCOL}, {ERROR_BFTP_INVALID_PROTOCOL, IDS_ERROR_PROTOCOL}, {ERROR_BFTP_NO_MORE_FRAGMENTS, IDS_ERROR_PROTOCOL}, {ERROR_SUCCESS, -1} };
////////////////////////////////////////////////////////////////////
//
//Miscellaneous useful functions
//
///////////////////////////////////////////////////////////////////
int ParseFileNames (TCHAR* pszInString, TCHAR* pszFilesList, int& iCharCount) {
ASSERT(pszFilesList != NULL); ASSERT(pszInString != NULL);
BOOL fInQuotes = FALSE; BOOL fIgnoreSpaces = FALSE; TCHAR* pszSource = pszInString; TCHAR* pszTarget = pszFilesList; int iFileCount = 0; TCHAR curr;
//ignore leading whitespaces
while(' ' == *pszSource || '\t' == *pszSource) pszSource++;
iCharCount = 0; *pszTarget = '\0'; //precautionary measures
if ('\0' == *pszSource) //special case : if this was an empty string, return 0
return iFileCount;
//parse the string to get filenames
while(curr = *pszSource) { if('\"' == curr) { fInQuotes = fInQuotes?FALSE:TRUE; } else if(' ' == curr && !fInQuotes) { if(!fIgnoreSpaces) { *pszTarget++ = 0; iFileCount++; iCharCount++; fIgnoreSpaces = TRUE; } } else { *pszTarget++ = curr; iCharCount++; fIgnoreSpaces = FALSE; }
pszSource++; }
if(' ' != *(pszSource-1)) //if there was no trailing space
{ *pszTarget++ = '\0'; //then the last file was not accounted for.
iCharCount++; //so we do it here
iFileCount++; }
*pszTarget++ = '\0'; //should have 2 terminating nulls
iCharCount++;
return iFileCount; }
//+--------------------------------------------------------------------------
//
// Function: GetIRRegVal
//
// Synopsis: gets the specified registry value from the IR subtree in HKCU
//
// Arguments: [in] szValName : the name of the value.
// [in] dwDefVal : the default value to be returned if the read
// from the registry fails or if the value is
// missing.
//
// Returns: the actual value stored in the registry or the default value
// if the read fails.
//
// History: 10/27/1999 RahulTh created
//
// Notes:
//
//---------------------------------------------------------------------------
DWORD GetIRRegVal (LPCTSTR szValName, DWORD dwDefVal) { HKEY hftKey = NULL; DWORD iSize = sizeof(DWORD); DWORD data = 0; DWORD Status;
RegOpenKeyEx (HKEY_CURRENT_USER, TEXT("Control Panel\\Infrared\\File Transfer"), 0, KEY_READ, &hftKey);
if (!hftKey) return dwDefVal;
Status = RegQueryValueEx (hftKey, szValName, NULL, NULL, (LPBYTE)&data, &iSize);
if (ERROR_SUCCESS != Status) data = dwDefVal;
RegCloseKey (hftKey);
return data; }
TCHAR* GetFullPathnames (TCHAR* pszPath, //directory in which the files are located
const TCHAR* pszFilesList, //NULL separated list of filenames
int iFileCount, //number of files in pszFilesList
int& iCharCount //number of characters in pszFilesList. also returns the number of chars in the return string
) { int iChars; int iPathLen = lstrlen(pszPath); if (pszPath[iPathLen - 1] != '\\') //append a '\' character to the path if not already present
{ pszPath[iPathLen++] = '\\'; pszPath[iPathLen] = '\0'; } int iSize = (iChars = iFileCount*iPathLen + iCharCount); TCHAR* pszFilePathList = new TCHAR[iSize]; TCHAR* pszTemp = pszFilePathList;
int iLen;
while(*pszFilesList) { //
// start with the path
//
StringCchCopy(pszTemp, iSize ,pszPath);
//
// add on the file name
//
StringCchCat(pszTemp, iSize, pszFilesList);
//
// move the next files name
//
iLen = lstrlen(pszFilesList); pszFilesList += iLen + 1;
//
// move past the current path and file name
//
iLen = lstrlen(pszTemp); pszTemp += iLen + 1; iSize-=iLen; } *pszTemp = '\0'; //should be terminated by 2 null characters
iCharCount = (int)(pszTemp - pszFilePathList) + 1; //return the actual char count of this string
return pszFilePathList; }
TCHAR* ProcessOneFile (TCHAR* pszPath, //directory in which the files are located
const TCHAR* pszFilesList, //NULL separated list of filenames
int iFileCount, //number of files in pszFilesList
int& iCharCount //number of characters in pszFilesList. also returns the number of characters in the return string
) { int iFileLen, iPathLen; TCHAR* pszFullFileName;
iFileLen = lstrlen (pszFilesList); iPathLen = lstrlen (pszPath); ASSERT (iFileLen); ASSERT (iPathLen);
if(':' == pszFilesList[1] //this is an absolute path starting with the drive letter;
|| ('\\' == pszFilesList[0] && '\\' == pszFilesList[1]) //UNC path
) { pszFullFileName = new TCHAR [iFileLen + 2];
StringCchCopy(pszFullFileName, iFileLen + 2, pszFilesList); pszFullFileName[iFileLen + 1] = '\0'; //we need to have 2 terminating nulls
iCharCount = iFileLen + 2; } else if('\\' == pszFilesList[0]) //path relative to the root
{ iCharCount = iFileLen + 2 /*drive letter and colon*/ + 2 /*terminating nulls*/; pszFullFileName = new TCHAR [iCharCount]; pszFullFileName[0] = pszPath[0]; pszFullFileName[1] = pszPath[1];
StringCchCopy(pszFullFileName + 2, iCharCount-2, pszFilesList); pszFullFileName[iCharCount - 1] = '\0'; //we need to have 2 terminating nulls
} else //ordinary file name
{ iCharCount = iPathLen + iFileLen + 2; //2 terminating nulls
//
// sometimes the path does not have a \ at the end, so we need to add these ourselves
//
iCharCount += ('\\' == pszPath[iPathLen - 1])?0:1;
pszFullFileName = new TCHAR [iCharCount];
//
// put in the path
//
StringCchCopy(pszFullFileName,iCharCount, pszPath);
if ('\\' != pszPath[iPathLen - 1]) { //
// we need to add the \ ourselves
//
StringCchCat(pszFullFileName,iCharCount,TEXT("\\"));
} //
// add on the file name
//
StringCchCat(pszFullFileName,iCharCount,pszFilesList);
pszFullFileName[iCharCount - 1] = '\0'; //2 terminating nulls
}
return pszFullFileName; }
//+--------------------------------------------------------------------------
//
// Function: GetPrimaryAppWindow
//
// Synopsis: gets the handle to the main window of an existing instance of
// irftp
//
// Arguments: none.
//
// Returns: handle to the window if it finds one. otherwise NULL.
//
// History: 6/30/1999 RahulTh created
//
// Notes:
//
//---------------------------------------------------------------------------
HWND GetPrimaryAppWindow (void) { HWND hwnd = NULL; int i = 1;
//try to find the window for 5 seconds.
do { hwnd = FindWindow (L"#32770", //the dialog class
MAIN_WINDOW_TITLE); if (hwnd) break; Sleep (500); } while ( i++ <= 10 );
return hwnd; }
///////////////////////////////////////////////////////////////////////////////////////
// RPC Server functions
//////////////////////////////////////////////////////////////////////////////////////
void _PopupUI (handle_t Binding) { int nResponse;
appController->PostMessage(WM_APP_TRIGGER_UI); return; }
void _InitiateFileTransfer (handle_t Binding, ULONG lSize, wchar_t __RPC_FAR lpszFilesList[]) { COPYDATASTRUCT cStruct; cStruct.dwData = lSize; cStruct.cbData = lSize * sizeof(wchar_t); cStruct.lpData = (LPVOID)(lpszFilesList); appController->SendMessage(WM_COPYDATA, (WPARAM)NULL, (LPARAM)(&cStruct)); }
void _DisplaySettings (handle_t Binding) { appController->PostMessage(WM_APP_TRIGGER_SETTINGS); }
void _UpdateSendProgress ( handle_t RpcBinding, COOKIE Cookie, wchar_t CurrentFile[], __int64 BytesInTransfer, __int64 BytesTransferred, error_status_t* pStatus ) { VALIDATE_SEND_COOKIE (Cookie)
CSendProgress* progressDlg = (CSendProgress*)Cookie; int percentComplete;
if (BytesInTransfer) { percentComplete = (int)((BytesTransferred*100.0)/BytesInTransfer); } else { percentComplete = 100; }
progressDlg->PostMessage(WM_APP_UPDATE_PROGRESS, (WPARAM) 0, (LPARAM) percentComplete); if (100 > percentComplete) { progressDlg->SetCurrentFileName (CurrentFile); } *pStatus = 0; }
void _OneSendFileFailed( handle_t RpcBinding, COOKIE Cookie, wchar_t FileName[], error_status_t ErrorCode, int Location, error_status_t * pStatus ) { VALIDATE_SEND_COOKIE (Cookie)
struct SEND_FAILURE_DATA Data;
COPYDATASTRUCT cStruct; CWnd* progressDlg = (CWnd*)Cookie;
StringCbCopy(Data.FileName,sizeof(Data.FileName), FileName); Data.Location = (FAILURE_LOCATION)Location; Data.Error = ErrorCode;
cStruct.cbData = sizeof(SEND_FAILURE_DATA); cStruct.lpData = &Data;
progressDlg->SendMessage(WM_COPYDATA, (WPARAM) 0, (LPARAM)(&cStruct)); *pStatus = 0; }
void _SendComplete( handle_t RpcBinding, COOKIE Cookie, __int64 BytesTransferred, error_status_t* pStatus ) { VALIDATE_SEND_COOKIE (Cookie)
CWnd* progressDlg = (CWnd*)Cookie; progressDlg->PostMessage(WM_APP_SEND_COMPLETE); *pStatus = 0; }
error_status_t _ReceiveInProgress( handle_t RpcBinding, wchar_t MachineName[], COOKIE * pCookie, boolean bSuppressRecvConf ) { struct MSG_RECEIVE_IN_PROGRESS msg;
msg.MachineName = MachineName; msg.pCookie = pCookie; msg.bSuppressRecvConf = bSuppressRecvConf; msg.status = ~0UL;
appController->SendMessage( WM_APP_RECV_IN_PROGRESS, (WPARAM) &msg );
return msg.status; }
error_status_t _GetPermission( handle_t RpcBinding, COOKIE Cookie, wchar_t Name[], boolean fDirectory ) { struct MSG_GET_PERMISSION msg;
msg.Cookie = Cookie; msg.Name = Name; msg.fDirectory = fDirectory; msg.status = ~0UL;
appController->SendMessage( WM_APP_GET_PERMISSION, (WPARAM) &msg );
return msg.status; }
error_status_t _ReceiveFinished( handle_t RpcBinding, COOKIE Cookie, error_status_t Status ) { struct MSG_RECEIVE_FINISHED msg;
msg.Cookie = Cookie; msg.ReceiveStatus = Status; msg.status = ~0UL;
appController->SendMessage( WM_APP_RECV_FINISHED, (WPARAM) &msg );
return msg.status; }
void _DeviceInRange( handle_t RpcBinding, POBEX_DEVICE_LIST device, error_status_t* pStatus ) { appController->PostMessage (WM_APP_KILL_TIMER);
LONG NewLinkCount = InterlockedIncrement(&g_lLinkOnDesktop); BOOL Result;
if (NewLinkCount == 1) { //
// the link count went from zero to 1, create the links now
//
Result=CreateLinks();
if (!Result) { //
// could not create the links
//
InterlockedExchange(&g_lLinkOnDesktop,0); } }
g_deviceList= device;
*pStatus = 0; }
void _NoDeviceInRange( handle_t RpcBinding, error_status_t* pStatus ) { InterlockedExchange(&g_lLinkOnDesktop,0);
RemoveLinks();
g_deviceList = NULL;
if (0 == g_lUIComponentCount) { //
// nothing displayed, start timer to shutdown app if no device comeback some.
//
appController->PostMessage (WM_APP_START_TIMER); }
*pStatus = 0; }
VOID CloseDownUI( VOID )
{
if (0 == g_lUIComponentCount) {
if (appController != NULL) {
appController->PostMessage (WM_CLOSE); }
} else {
if (appController != NULL) {
appController->SessionOver(); } }
return; }
void _Message( handle_t RpcBinding, wchar_t String[] ) { AFX_MANAGE_STATE (AfxGetStaticModuleState());
CString szTitle;
szTitle.LoadString (IDS_CAPTION);
InterlockedIncrement (&g_lUIComponentCount); ::MessageBox (NULL, String, (LPCTSTR) szTitle, MB_OK); BOOL fNoUIComponents = (0 == InterlockedDecrement (&g_lUIComponentCount)); if (appController && fNoUIComponents && ! g_deviceList.GetDeviceCount()) { //there are no UI components displayed and there are no devices in
//range. Start the timer. If the timer expires, the app. will quit.
appController->PostMessage (WM_APP_START_TIMER); }
}
error_status_t _ShutdownUi(handle_t RpcBinding) { appController->PostMessage( WM_CLOSE ); return 0; }
error_status_t _ShutdownRequested( handle_t RpcBinding, boolean * pAnswer ) { WCHAR pwszCaption [50]; WCHAR pwszMessage [MAX_PATH];
*pAnswer = TRUE;
if (appController) { appController->PostMessage (WM_APP_KILL_TIMER); }
if (! ::LoadString ( g_hInstance, IDS_CAPTION, pwszCaption, 50)) { return ERROR_NOT_ENOUGH_MEMORY; }
if (! ::LoadString ( g_hInstance, IDS_SHUTDOWN_MESSAGE, pwszMessage, MAX_PATH)) { return ERROR_NOT_ENOUGH_MEMORY; }
//display a message box with YES / NO buttons
if (IDYES == ::MessageBox (appController->m_hWnd, pwszMessage, pwszCaption, MB_ICONEXCLAMATION | MB_YESNO | MB_SYSTEMMODAL | MB_SETFOREGROUND)) { *pAnswer = TRUE; } else { *pAnswer = FALSE; }
return 0; }
//////////////////////////////////////////////////////////////////////////////
//Create links on the desktop and in the Send To menu to this executable file
BOOL CreateLinks( VOID ) { AFX_MANAGE_STATE (AfxGetStaticModuleState()); HRESULT Result=E_FAIL; LONG i;
TCHAR lpszFullExeName [2*MAX_PATH]; TCHAR lpszShortcutName[2*MAX_PATH]; CString szDesc;
szDesc.LoadString (IDS_SHTCUT_DESC);
//
// create the desktop link
//
if (GetShortcutInfo(lpszShortcutName,sizeof(lpszShortcutName)/sizeof(TCHAR), lpszFullExeName,sizeof(lpszFullExeName)/sizeof(TCHAR))) {
#if 0
OutputDebugString(lpszShortcutName); OutputDebugStringA("\n"); #endif
Result=CreateShortcut (lpszFullExeName, lpszShortcutName, (LPCTSTR) szDesc);
if (SUCCEEDED(Result)) {
} else { #if DB
OutputDebugStringA("Could not create desktop link\n"); #endif
}
} else { #if DBG
OutputDebugStringA("Could not get desktop path\n"); #endif
}
if (!SUCCEEDED(Result)) {
return FALSE; }
//
// create the send to link
//
if (GetSendToInfo(lpszShortcutName,sizeof(lpszShortcutName)/sizeof(TCHAR) ,lpszFullExeName,sizeof(lpszFullExeName)/sizeof(TCHAR))) {
Result=CreateShortcut (lpszFullExeName, lpszShortcutName, (LPCTSTR) szDesc); if (SUCCEEDED(Result)) {
} else { #if DBG
OutputDebugStringA("Could not create desktop link\n"); #endif
}
} else { #if DBG
OutputDebugStringA("Could get sendto path\n"); #endif
}
return SUCCEEDED(Result); }
//////////////////////////////////////////////////////////////////////////////
// CreateShortcut - uses the shell's IShellLink and IPersistFile interfaces
// to create and store a shortcut to the specified object.
HRESULT CreateShortcut (LPCTSTR lpszExe, LPCTSTR lpszLink, LPCTSTR lpszDesc) { HRESULT hres; IShellLink* psl;
hres = CoInitialize(NULL);
if (FAILED(hres)) return hres;
// Get a pointer to the IShellLink interface.
hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl); if (SUCCEEDED(hres)) { IPersistFile* ppf;
// Set the path to the shortcut target and add the
// description.
psl->SetPath(lpszExe); psl->SetDescription(lpszDesc);
// Query IShellLink for the IPersistFile interface for saving the
// shortcut in persistent storage.
hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf);
if (SUCCEEDED(hres)) { // Save the link by calling IPersistFile::Save.
hres = ppf->Save(lpszLink, TRUE); ppf->Release(); } psl->Release(); } return SUCCEEDED(hres)?S_OK:E_FAIL; }
void RemoveLinks (void) { TCHAR lpszShortcutName[2 * MAX_PATH]; TCHAR lpszFullExeName[2 * MAX_PATH];
//delete the desktop shortcut
if (GetShortcutInfo (lpszShortcutName,sizeof(lpszShortcutName)/sizeof(TCHAR), lpszFullExeName,sizeof(lpszFullExeName)/sizeof(TCHAR))) {
DeleteFile (lpszShortcutName); }
//delete the send to shortcut
//
if (GetSendToInfo (lpszShortcutName,sizeof(lpszShortcutName)/sizeof(TCHAR), lpszFullExeName,sizeof(lpszFullExeName)/sizeof(TCHAR))) {
DeleteFile (lpszShortcutName); } }
BOOL GetShortcutInfo ( LPTSTR lpszShortcutName, ULONG ShortCutNameLength, LPTSTR lpszFullExeName, ULONG ExeNameLength )
{ AFX_MANAGE_STATE (AfxGetStaticModuleState());
*lpszShortcutName = '\0'; //precautionary measures
*lpszFullExeName = '\0'; CString szExe; CString szShtCut; int len;
szExe.LoadString (IDS_EXE); szShtCut.LoadString (IDS_DESKTOP_SHTCUT);
len = GetSystemDirectory (lpszFullExeName, ExeNameLength); if (0 == len) {
return FALSE; }
StringCchCat(lpszFullExeName,ExeNameLength,LPCTSTR (szExe));
if('\0' == g_lpszDesktopFolder[0]) //try once again if we had failed before, or maybe this is the first time
{ if (!SHGetSpecialFolderPath(NULL, g_lpszDesktopFolder, CSIDL_DESKTOPDIRECTORY, 0)) { g_lpszDesktopFolder[0] = '\0'; //we failed so give up.
return FALSE; } }
StringCchCopy(lpszShortcutName,ShortCutNameLength, g_lpszDesktopFolder); StringCchCat(lpszShortcutName,ShortCutNameLength, (LPCTSTR) szShtCut);
return TRUE; }
BOOL GetSendToInfo ( LPTSTR lpszSendToName, ULONG SendToNameLength, LPTSTR lpszFullExeName, ULONG FullExeNameLength )
{ AFX_MANAGE_STATE (AfxGetStaticModuleState());
*lpszSendToName = TEXT('\0'); //precautionary measures
*lpszFullExeName = TEXT('\0'); CString szExe; CString szSendTo; int len;
szExe.LoadString (IDS_EXE); szSendTo.LoadString (IDS_SENDTO_SHTCUT);
len = GetSystemDirectory (lpszFullExeName, FullExeNameLength); if (0 == len) {
return FALSE; }
StringCchCat(lpszFullExeName,FullExeNameLength, (LPCTSTR) szExe);
if ('\0' == g_lpszSendToFolder[0]) //try once again if we had failed before, or maybe this is the first time
{ if (!SHGetSpecialFolderPath(NULL, g_lpszSendToFolder, CSIDL_SENDTO, 0)) { g_lpszSendToFolder[0] = TEXT('\0'); return FALSE; } }
StringCchCopy(lpszSendToName,SendToNameLength, g_lpszSendToFolder); StringCchCat(lpszSendToName, SendToNameLength,(LPCTSTR) szSendTo);
return TRUE; }
//+--------------------------------------------------------------------------
//
// Member: CError::ConstructMessage
//
// Synopsis: this is an internal helper function that constructs a message
// from the available error codes it is called by both ShowMessage
//
// Arguments: [in] argList : the va_list of arguments
// [out] szErrMsg : the formatted error message
//
// Returns: nothing
//
// History: 10/2/1998 RahulTh created
//
// Notes:
//
//---------------------------------------------------------------------------
void CError::ConstructMessage (va_list argList, CString& szErrMsg) { AFX_MANAGE_STATE (AfxGetStaticModuleState());
TCHAR lpszMessage[2048];
szErrMsg.LoadString (m_msgID);
StringCbVPrintf(lpszMessage, sizeof(lpszMessage),(LPCTSTR) szErrMsg, argList);
szErrMsg = lpszMessage;
if (ERROR_SUCCESS != m_winErr) { LPVOID lpMsgBuf; DWORD dwRet; dwRet = ::FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, m_winErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMsgBuf, 0, NULL ); if (dwRet) { szErrMsg += TEXT("\n\n"); szErrMsg += (LPCTSTR) lpMsgBuf; LocalFree (lpMsgBuf); } } }
//+--------------------------------------------------------------------------
//
// Member: CError::ShowMessage
//
// Synopsis: displays an error message in a message box based on the
// members of the object
//
// Arguments: message id for the error + more
//
// Returns: the return value of the message box
//
// History: 10/1/1998 RahulTh created
//
// Notes: if the resultant message is longer than 2048 characters
// then result is unpredictable and may also cause AVs.
// but this is a limitation of wvsprintf. However, this is not
// so bad since we can make sure that we do not have any error
// message that exceed this self imposed limit
//
//---------------------------------------------------------------------------
int CError::ShowMessage (UINT errID, ...) { AFX_MANAGE_STATE (AfxGetStaticModuleState());
va_list argList; CString szErrMsg; CString szTitle;
m_msgID = errID; //update the message ID with the new one
szTitle.LoadString (m_titleID);
va_start (argList, errID); ConstructMessage (argList, szErrMsg); va_end (argList);
return ::MessageBox (m_hWndParent, (LPCTSTR)szErrMsg, (LPCTSTR) szTitle, m_nStyle); }
|