Source code of Windows XP (NT5)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

661 lines
19 KiB

  1. //+---------------------------------------------------------------------------
  2. //
  3. // Microsoft Windows
  4. // Copyright (C) Microsoft Corporation, 2000.
  5. //
  6. // File: D E V I C E P E R S I S T E N C E M A N A G E R . C P P
  7. //
  8. // Contents: Persistence for UPnP device host registrar settings to registry
  9. //
  10. // Notes:
  11. //
  12. // Author: mbend 6 Sep 2000
  13. //
  14. //----------------------------------------------------------------------------
  15. #include "pch.h"
  16. #pragma hdrstop
  17. #include "uhbase.h"
  18. #include "hostp.h"
  19. #include "DevicePersistenceManager.h"
  20. #include "uhsync.h"
  21. #include "ncreg.h"
  22. #include "Array.h"
  23. #include "ComUtility.h"
  24. #include "uhutil.h"
  25. #include "uhcommon.h"
  26. // String constants
  27. const wchar_t c_szDevices[] = L"Devices";
  28. const wchar_t c_szProviders[] = L"Providers";
  29. const wchar_t c_szProgId[] = L"ProgId";
  30. const wchar_t c_szInitString[] = L"Init String";
  31. const wchar_t c_szContainerId[] = L"Container Id";
  32. const wchar_t c_szResourcePath[] = L"Resource Path";
  33. const wchar_t c_szLifeTime[] = L"Life Time";
  34. const wchar_t c_szProgIdProviderClass[] = L"Provider ProgId";
  35. // Helper functions
  36. HRESULT HrCreateOrOpenDevicesKey(HKEY * phKeyDevices)
  37. {
  38. CHECK_POINTER(phKeyDevices);
  39. HRESULT hr = S_OK;
  40. HKEY hKeyDeviceHost;
  41. hr = HrCreateOrOpenDeviceHostKey(&hKeyDeviceHost);
  42. if(SUCCEEDED(hr))
  43. {
  44. HKEY hKeyDevices;
  45. DWORD dwDisposition = 0;
  46. hr = HrRegCreateKeyEx(hKeyDeviceHost, c_szDevices, 0, KEY_ALL_ACCESS, NULL, &hKeyDevices, &dwDisposition);
  47. if(SUCCEEDED(hr))
  48. {
  49. *phKeyDevices = hKeyDevices;
  50. }
  51. RegCloseKey(hKeyDeviceHost);
  52. }
  53. TraceHr(ttidError, FAL, hr, FALSE, "HrCreateOrOpenDevicesKey");
  54. return hr;
  55. }
  56. HRESULT HrCreateOrOpenProvidersKey(HKEY * phKeyProviders)
  57. {
  58. CHECK_POINTER(phKeyProviders);
  59. HRESULT hr = S_OK;
  60. HKEY hKeyDeviceHost;
  61. hr = HrCreateOrOpenDeviceHostKey(&hKeyDeviceHost);
  62. if(SUCCEEDED(hr))
  63. {
  64. HKEY hKeyProviders;
  65. DWORD dwDisposition = 0;
  66. hr = HrRegCreateKeyEx(hKeyDeviceHost, c_szProviders, 0, KEY_ALL_ACCESS, NULL, &hKeyProviders, &dwDisposition);
  67. if(SUCCEEDED(hr))
  68. {
  69. *phKeyProviders = hKeyProviders;
  70. }
  71. RegCloseKey(hKeyDeviceHost);
  72. }
  73. TraceHr(ttidError, FAL, hr, FALSE, "HrCreateOrOpenProvidersKey");
  74. return hr;
  75. }
  76. CDevicePersistenceManager::CDevicePersistenceManager()
  77. {
  78. }
  79. CDevicePersistenceManager::~CDevicePersistenceManager()
  80. {
  81. }
  82. STDMETHODIMP CDevicePersistenceManager::SavePhyisicalDevice(
  83. /*[in]*/ REFGUID guidPhysicalDeviceIdentifier,
  84. /*[in, string]*/ const wchar_t * szProgIdDeviceControlClass,
  85. /*[in, string]*/ const wchar_t * szInitString,
  86. /*[in, string]*/ const wchar_t * szContainerId,
  87. /*[in, string]*/ const wchar_t * szResourcePath,
  88. /*[in]*/ long nLifeTime)
  89. {
  90. HRESULT hr = S_OK;
  91. // Create the string to use
  92. CUString strUuid;
  93. hr = HrIsAllowedCOMCallLocality((CALL_LOCALITY) CALL_LOCALITY_INPROC);
  94. if (SUCCEEDED(hr))
  95. {
  96. hr = strUuid.HrInitFromGUID(guidPhysicalDeviceIdentifier);
  97. }
  98. if(SUCCEEDED(hr))
  99. {
  100. HKEY hKeyDevices;
  101. hr = HrCreateOrOpenDevicesKey(&hKeyDevices);
  102. if(SUCCEEDED(hr))
  103. {
  104. // Create key to house values
  105. HKEY hKeyPid;
  106. DWORD dwDisposition = 0;
  107. hr = HrRegCreateKeyEx(hKeyDevices, strUuid, 0, KEY_ALL_ACCESS, NULL, &hKeyPid, &dwDisposition);
  108. if(SUCCEEDED(hr))
  109. {
  110. // Save all of the values
  111. hr = HrRegSetSz(hKeyPid, c_szProgId, szProgIdDeviceControlClass);
  112. if(SUCCEEDED(hr))
  113. {
  114. hr = HrRegSetSz(hKeyPid, c_szInitString, szInitString);
  115. if(SUCCEEDED(hr))
  116. {
  117. hr = HrRegSetSz(hKeyPid, c_szContainerId, szContainerId);
  118. if(SUCCEEDED(hr))
  119. {
  120. hr = HrRegSetSz(hKeyPid, c_szResourcePath, szResourcePath);
  121. if(SUCCEEDED(hr))
  122. {
  123. hr = HrRegSetDword(hKeyPid, c_szLifeTime, nLifeTime);
  124. }
  125. }
  126. }
  127. }
  128. RegCloseKey(hKeyPid);
  129. if(FAILED(hr))
  130. {
  131. // If anything fails, remove the whole tree
  132. HrRegDeleteKeyTree(hKeyDevices, strUuid);
  133. }
  134. }
  135. RegCloseKey(hKeyDevices);
  136. }
  137. }
  138. TraceHr(ttidError, FAL, hr, FALSE, "CDevicePersistenceManager::SavePhyisicalDevice");
  139. return hr;
  140. }
  141. STDMETHODIMP CDevicePersistenceManager::LookupPhysicalDevice(
  142. /*[in]*/ REFGUID guidPhysicalDeviceIdentifier,
  143. /*[out, string]*/ wchar_t ** pszProgIdDeviceControlClass,
  144. /*[out, string]*/ wchar_t ** pszInitString,
  145. /*[out, string]*/ wchar_t ** pszContainerId,
  146. /*[out, string]*/ wchar_t ** pszResourcePath,
  147. /*[out]*/ long * pnLifeTime)
  148. {
  149. CHECK_POINTER(pszProgIdDeviceControlClass);
  150. CHECK_POINTER(pszInitString);
  151. CHECK_POINTER(pszContainerId);
  152. CHECK_POINTER(pszResourcePath);
  153. CHECK_POINTER(pnLifeTime);
  154. HRESULT hr = S_OK;
  155. // Create the string to use
  156. CUString strUuid;
  157. CUString strProgIdDeviceControlClass;
  158. CUString strInitString;
  159. CUString strContainerId;
  160. CUString strResourcePath;
  161. DWORD dwLifeTime;
  162. hr = HrIsAllowedCOMCallLocality((CALL_LOCALITY) CALL_LOCALITY_INPROC);
  163. if (SUCCEEDED(hr))
  164. {
  165. hr = strUuid.HrInitFromGUID(guidPhysicalDeviceIdentifier);
  166. }
  167. if(SUCCEEDED(hr))
  168. {
  169. HKEY hKeyDevices;
  170. hr = HrCreateOrOpenDevicesKey(&hKeyDevices);
  171. if(SUCCEEDED(hr))
  172. {
  173. // Open the key housing the values
  174. HKEY hKeyPid;
  175. DWORD dwDisposition = 0;
  176. hr = HrRegOpenKeyEx(hKeyDevices, strUuid, KEY_ALL_ACCESS, &hKeyPid);
  177. if(SUCCEEDED(hr))
  178. {
  179. // Load all of the values
  180. hr = HrRegQueryString(hKeyPid, c_szProgId, strProgIdDeviceControlClass);
  181. if(SUCCEEDED(hr))
  182. {
  183. hr = HrRegQueryString(hKeyPid, c_szInitString, strInitString);
  184. if(SUCCEEDED(hr))
  185. {
  186. hr = HrRegQueryString(hKeyPid, c_szContainerId, strContainerId);
  187. if(SUCCEEDED(hr))
  188. {
  189. hr = HrRegQueryString(hKeyPid, c_szResourcePath, strResourcePath);
  190. if(SUCCEEDED(hr))
  191. {
  192. hr = HrRegQueryDword(hKeyPid, c_szLifeTime, &dwLifeTime);
  193. }
  194. }
  195. }
  196. }
  197. RegCloseKey(hKeyPid);
  198. }
  199. RegCloseKey(hKeyDevices);
  200. }
  201. }
  202. // Set strings to NULL
  203. *pszProgIdDeviceControlClass = NULL;
  204. *pszInitString = NULL;
  205. *pszContainerId = NULL;
  206. *pszResourcePath = NULL;
  207. // On success set the out params
  208. if(SUCCEEDED(hr))
  209. {
  210. hr = strProgIdDeviceControlClass.HrGetCOM(pszProgIdDeviceControlClass);
  211. if(SUCCEEDED(hr))
  212. {
  213. hr = strInitString.HrGetCOM(pszInitString);
  214. if(SUCCEEDED(hr))
  215. {
  216. hr = strContainerId.HrGetCOM(pszContainerId);
  217. if(SUCCEEDED(hr))
  218. {
  219. hr = strResourcePath.HrGetCOM(pszResourcePath);
  220. if(SUCCEEDED(hr))
  221. {
  222. *pnLifeTime = dwLifeTime;
  223. }
  224. }
  225. }
  226. }
  227. if(FAILED(hr))
  228. {
  229. // If one fails, they all fail
  230. if(pszInitString)
  231. {
  232. CoTaskMemFree(pszInitString);
  233. *pszInitString = NULL;
  234. }
  235. if(pszContainerId)
  236. {
  237. CoTaskMemFree(pszContainerId);
  238. *pszContainerId = NULL;
  239. }
  240. if(pszResourcePath)
  241. {
  242. CoTaskMemFree(pszResourcePath);
  243. *pszResourcePath = NULL;
  244. }
  245. }
  246. }
  247. TraceHr(ttidError, FAL, hr, FALSE, "CDevicePersistenceManager::LookupPhysicalDevice");
  248. return hr;
  249. }
  250. STDMETHODIMP CDevicePersistenceManager::RemovePhysicalDevice(
  251. /*[in]*/ REFGUID guidPhysicalDeviceIdentifier)
  252. {
  253. HRESULT hr = S_OK;
  254. // Create the string to use
  255. CUString strUuid;
  256. hr = HrIsAllowedCOMCallLocality((CALL_LOCALITY) CALL_LOCALITY_INPROC);
  257. if (SUCCEEDED(hr))
  258. {
  259. hr = strUuid.HrInitFromGUID(guidPhysicalDeviceIdentifier);
  260. }
  261. if(SUCCEEDED(hr))
  262. {
  263. HKEY hKeyDevices;
  264. hr = HrCreateOrOpenDevicesKey(&hKeyDevices);
  265. if(SUCCEEDED(hr))
  266. {
  267. hr = HrRegDeleteKeyTree(hKeyDevices, strUuid);
  268. RegCloseKey(hKeyDevices);
  269. }
  270. }
  271. TraceHr(ttidError, FAL, hr, FALSE, "CDevicePersistenceManager::RemovePhysicalDevice");
  272. return hr;
  273. }
  274. STDMETHODIMP CDevicePersistenceManager::GetPhysicalDevices(
  275. /*[out]*/ long * pnDevices,
  276. /*[out, size_is(,*pnDevices)]*/
  277. GUID ** parguidPhysicalDeviceIdentifiers)
  278. {
  279. CHECK_POINTER(pnDevices);
  280. CHECK_POINTER(parguidPhysicalDeviceIdentifiers);
  281. // Set this to NULL at the beginning
  282. *parguidPhysicalDeviceIdentifiers = NULL;
  283. HRESULT hr = S_OK;
  284. // Do work in an array
  285. CUArray<GUID> arPids;
  286. HKEY hKeyDevices;
  287. hr = HrIsAllowedCOMCallLocality((CALL_LOCALITY) CALL_LOCALITY_INPROC);
  288. if (SUCCEEDED(hr))
  289. {
  290. // Open devices key
  291. hr = HrCreateOrOpenDevicesKey(&hKeyDevices);
  292. }
  293. if(SUCCEEDED(hr))
  294. {
  295. DWORD dwSize;
  296. wchar_t szBuf[_MAX_PATH];
  297. FILETIME ft;
  298. DWORD dwIndex = 0;
  299. UUID uuid;
  300. while(SUCCEEDED(hr))
  301. {
  302. // Enumerate all of the keys
  303. dwSize = _MAX_PATH;
  304. hr = HrRegEnumKeyEx(hKeyDevices, dwIndex, szBuf, &dwSize, NULL, NULL, &ft);
  305. ++dwIndex;
  306. if(S_OK == hr)
  307. {
  308. hr = CLSIDFromString(szBuf, &uuid);
  309. if(SUCCEEDED(hr))
  310. {
  311. hr = arPids.HrPushBack(uuid);
  312. }
  313. else
  314. {
  315. TraceHr(ttidRegistrar, FAL, hr, FALSE, "CDevicePersistenceManager::GetPhysicalDevices - CLSIDFromString failed!");
  316. hr = S_OK;
  317. // Ignore and skip it - this shouldn't happen
  318. continue;
  319. }
  320. }
  321. else
  322. {
  323. // This is not an error, we are out of subkeys
  324. hr = S_OK;
  325. break;
  326. }
  327. }
  328. RegCloseKey(hKeyDevices);
  329. }
  330. // Attempt to copy to output parameters
  331. if(SUCCEEDED(hr))
  332. {
  333. long nCount = arPids.GetCount();
  334. if(nCount)
  335. {
  336. // Allocate output array
  337. HrCoTaskMemAllocArray(nCount, parguidPhysicalDeviceIdentifiers);
  338. // Fill in array
  339. for(long n = 0; n < nCount; ++n)
  340. {
  341. (*parguidPhysicalDeviceIdentifiers)[n] = arPids[n];
  342. }
  343. }
  344. else
  345. {
  346. *parguidPhysicalDeviceIdentifiers = NULL;
  347. }
  348. *pnDevices = nCount;
  349. }
  350. if(FAILED(hr))
  351. {
  352. *pnDevices = 0;
  353. if(*parguidPhysicalDeviceIdentifiers)
  354. {
  355. CoTaskMemFree(*parguidPhysicalDeviceIdentifiers);
  356. }
  357. *parguidPhysicalDeviceIdentifiers = NULL;
  358. }
  359. TraceHr(ttidError, FAL, hr, FALSE, "CDevicePersistenceManager::GetPhysicalDevices");
  360. return hr;
  361. }
  362. STDMETHODIMP CDevicePersistenceManager::SaveDeviceProvider(
  363. /*[in, string]*/ const wchar_t * szProviderName,
  364. /*[in, string]*/ const wchar_t * szProgIdProviderClass,
  365. /*[in, string]*/ const wchar_t * szInitString,
  366. /*[in, string]*/ const wchar_t * szContainerId)
  367. {
  368. CHECK_POINTER(szProviderName);
  369. CHECK_POINTER(szProgIdProviderClass);
  370. CHECK_POINTER(szInitString);
  371. CHECK_POINTER(szContainerId);
  372. HRESULT hr = S_OK;
  373. HKEY hKeyProviders;
  374. hr = HrIsAllowedCOMCallLocality((CALL_LOCALITY) CALL_LOCALITY_INPROC);
  375. if (SUCCEEDED(hr))
  376. {
  377. hr = HrCreateOrOpenProvidersKey(&hKeyProviders);
  378. }
  379. if(SUCCEEDED(hr))
  380. {
  381. // Create key to house values
  382. HKEY hKeyProviderName;
  383. DWORD dwDisposition = 0;
  384. hr = HrRegCreateKeyEx(hKeyProviders, szProviderName, 0, KEY_ALL_ACCESS, NULL, &hKeyProviderName, &dwDisposition);
  385. if(SUCCEEDED(hr))
  386. {
  387. // Save all of the values
  388. hr = HrRegSetSz(hKeyProviderName, c_szProgIdProviderClass, szProgIdProviderClass);
  389. if(SUCCEEDED(hr))
  390. {
  391. hr = HrRegSetSz(hKeyProviderName, c_szInitString, szInitString);
  392. if(SUCCEEDED(hr))
  393. {
  394. hr = HrRegSetSz(hKeyProviderName, c_szContainerId, szContainerId);
  395. }
  396. }
  397. RegCloseKey(hKeyProviderName);
  398. if(FAILED(hr))
  399. {
  400. // If anything fails, remove the whole tree
  401. HrRegDeleteKeyTree(hKeyProviders, szProviderName);
  402. }
  403. }
  404. RegCloseKey(hKeyProviders);
  405. }
  406. TraceHr(ttidError, FAL, hr, FALSE, "CDevicePersistenceManager::SaveDeviceProvider");
  407. return hr;
  408. }
  409. STDMETHODIMP CDevicePersistenceManager::LookupDeviceProvider(
  410. /*[in, string]*/ const wchar_t * szProviderName,
  411. /*[out, string]*/ wchar_t ** pszProgIdProviderClass,
  412. /*[out, string]*/ wchar_t ** pszInitString,
  413. /*[out, string]*/ wchar_t ** pszContainerId)
  414. {
  415. CHECK_POINTER(szProviderName);
  416. CHECK_POINTER(pszProgIdProviderClass);
  417. CHECK_POINTER(pszInitString);
  418. CHECK_POINTER(pszContainerId);
  419. HRESULT hr = S_OK;
  420. CUString strProgIdProviderClass;
  421. CUString strInitString;
  422. CUString strContainerId;
  423. HKEY hKeyProviders;
  424. hr = HrIsAllowedCOMCallLocality((CALL_LOCALITY) CALL_LOCALITY_INPROC);
  425. if (SUCCEEDED(hr))
  426. {
  427. hr = HrCreateOrOpenProvidersKey(&hKeyProviders);
  428. }
  429. if(SUCCEEDED(hr))
  430. {
  431. // Open the key housing the values
  432. HKEY hKeyProviderName;
  433. DWORD dwDisposition = 0;
  434. hr = HrRegOpenKeyEx(hKeyProviders, szProviderName, KEY_ALL_ACCESS, &hKeyProviderName);
  435. if(SUCCEEDED(hr))
  436. {
  437. // Load all of the values
  438. hr = HrRegQueryString(hKeyProviderName, c_szProgIdProviderClass, strProgIdProviderClass);
  439. if(SUCCEEDED(hr))
  440. {
  441. hr = HrRegQueryString(hKeyProviderName, c_szInitString, strInitString);
  442. if(SUCCEEDED(hr))
  443. {
  444. hr = HrRegQueryString(hKeyProviderName, c_szContainerId, strContainerId);
  445. }
  446. }
  447. RegCloseKey(hKeyProviderName);
  448. }
  449. RegCloseKey(hKeyProviders);
  450. }
  451. // Set strings to NULL
  452. *pszProgIdProviderClass = NULL;
  453. *pszInitString = NULL;
  454. *pszContainerId = NULL;
  455. // On success set the out params
  456. if(SUCCEEDED(hr))
  457. {
  458. hr = strProgIdProviderClass.HrGetCOM(pszProgIdProviderClass);
  459. if(SUCCEEDED(hr))
  460. {
  461. hr = strInitString.HrGetCOM(pszInitString);
  462. if(SUCCEEDED(hr))
  463. {
  464. hr = strContainerId.HrGetCOM(pszContainerId);
  465. }
  466. }
  467. if(FAILED(hr))
  468. {
  469. // If one fails, they all fail
  470. if(pszInitString)
  471. {
  472. CoTaskMemFree(pszInitString);
  473. *pszInitString = NULL;
  474. }
  475. if(pszContainerId)
  476. {
  477. CoTaskMemFree(pszContainerId);
  478. *pszContainerId = NULL;
  479. }
  480. }
  481. }
  482. TraceHr(ttidError, FAL, hr, FALSE, "CDevicePersistenceManager::LookupDeviceProvider");
  483. return hr;
  484. }
  485. STDMETHODIMP CDevicePersistenceManager::RemoveDeviceProvider(
  486. /*[in, string]*/ const wchar_t * szProviderName)
  487. {
  488. HRESULT hr = S_OK;
  489. HKEY hKeyProviders;
  490. hr = HrIsAllowedCOMCallLocality((CALL_LOCALITY) CALL_LOCALITY_INPROC);
  491. if (SUCCEEDED(hr))
  492. {
  493. hr = HrCreateOrOpenProvidersKey(&hKeyProviders);
  494. }
  495. if(SUCCEEDED(hr))
  496. {
  497. hr = HrRegDeleteKeyTree(hKeyProviders, szProviderName);
  498. RegCloseKey(hKeyProviders);
  499. }
  500. TraceHr(ttidError, FAL, hr, FALSE, "CDevicePersistenceManager::RemoveDeviceProvider");
  501. return hr;
  502. }
  503. STDMETHODIMP CDevicePersistenceManager::GetDeviceProviders(
  504. /*[out]*/ long * pnProviders,
  505. /*[out, string, size_is(,*pnProviders,)]*/
  506. wchar_t *** parszProviderNames)
  507. {
  508. CHECK_POINTER(pnProviders);
  509. CHECK_POINTER(parszProviderNames);
  510. // Set this to NULL at the beginning
  511. *parszProviderNames = NULL;
  512. HRESULT hr = S_OK;
  513. // Do work in an array
  514. CUArray<wchar_t*> arszProviders;
  515. HKEY hKeyProviders;
  516. hr = HrIsAllowedCOMCallLocality((CALL_LOCALITY) CALL_LOCALITY_INPROC);
  517. if (SUCCEEDED(hr))
  518. {
  519. // Open devices key
  520. hr = HrCreateOrOpenProvidersKey(&hKeyProviders);
  521. }
  522. if(SUCCEEDED(hr))
  523. {
  524. DWORD dwSize;
  525. wchar_t szBuf[_MAX_PATH];
  526. FILETIME ft;
  527. DWORD dwIndex = 0;
  528. while(SUCCEEDED(hr))
  529. {
  530. // Enumerate all of the keys
  531. dwSize = _MAX_PATH;
  532. hr = HrRegEnumKeyEx(hKeyProviders, dwIndex, szBuf, &dwSize, NULL, NULL, &ft);
  533. if(S_OK == hr)
  534. {
  535. wchar_t * sz = NULL;
  536. hr = HrCoTaskMemAllocString(szBuf, &sz);
  537. if(SUCCEEDED(hr))
  538. {
  539. // Insert pointer to dynamically allocated string
  540. hr = arszProviders.HrPushBack(sz);
  541. }
  542. }
  543. else
  544. {
  545. // This is not an error, we have no more subkeys
  546. hr = S_OK;
  547. break;
  548. }
  549. ++dwIndex;
  550. }
  551. RegCloseKey(hKeyProviders);
  552. }
  553. // Attempt to copy to output parameters
  554. if(SUCCEEDED(hr))
  555. {
  556. long nCount = arszProviders.GetCount();
  557. if(nCount)
  558. {
  559. // Allocate output array
  560. HrCoTaskMemAllocArray(nCount, parszProviderNames);
  561. // Fill in array
  562. for(long n = 0; n < nCount; ++n)
  563. {
  564. (*parszProviderNames)[n] = arszProviders[n];
  565. }
  566. }
  567. else
  568. {
  569. *parszProviderNames = NULL;
  570. }
  571. *pnProviders = nCount;
  572. }
  573. if(FAILED(hr))
  574. {
  575. *pnProviders = 0;
  576. if(*parszProviderNames)
  577. {
  578. CoTaskMemFree(*parszProviderNames);
  579. }
  580. *parszProviderNames = NULL;
  581. // Cleanup dynamically allocated strings
  582. long nCount = arszProviders.GetCount();
  583. for(long n = 0; n < nCount; ++n)
  584. {
  585. CoTaskMemFree(arszProviders[n]);
  586. }
  587. }
  588. TraceHr(ttidError, FAL, hr, FALSE, "CDevicePersistenceManager::GetDeviceProviders");
  589. return hr;
  590. }