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.
 
 

508 lines
26 KiB

  1. /* Copyright (C) 2008-2016 Peter Palotas, Jeffrey Jangli, Alexandr Normuradov
  2. *
  3. * Permission is hereby granted, free of charge, to any person obtaining a copy
  4. * of this software and associated documentation files (the "Software"), to deal
  5. * in the Software without restriction, including without limitation the rights
  6. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  7. * copies of the Software, and to permit persons to whom the Software is
  8. * furnished to do so, subject to the following conditions:
  9. *
  10. * The above copyright notice and this permission notice shall be included in
  11. * all copies or substantial portions of the Software.
  12. *
  13. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  19. * THE SOFTWARE.
  20. */
  21. using Alphaleonis.Win32.Filesystem;
  22. using System;
  23. using System.Diagnostics.CodeAnalysis;
  24. using System.Globalization;
  25. using System.Net;
  26. using System.Net.NetworkInformation;
  27. using System.Security;
  28. using System.Text;
  29. namespace Alphaleonis.Win32.Network
  30. {
  31. partial class Host
  32. {
  33. #region ConnectDrive
  34. /// <summary>Creates a connection to a network resource. The function can redirect a local device to a network resource.</summary>
  35. /// <returns>If <paramref name="localName"/> is <see langword="null"/> or <c>string.Empty</c>, returns the last available drive letter, <see langword="null"/> otherwise.</returns>
  36. /// <param name="localName">
  37. /// The name of a local device to be redirected, such as "F:". When <paramref name="localName"/> is <see langword="null"/> or
  38. /// <c>string.Empty</c>, the last available drive letter will be used. Letters are assigned beginning with Z:, then Y: and so on.
  39. /// </param>
  40. /// <param name="remoteName">The network resource to connect to. The string can be up to MAX_PATH characters in length.</param>
  41. [SecurityCritical]
  42. public static string ConnectDrive(string localName, string remoteName)
  43. {
  44. return ConnectDisconnectCore(new ConnectDisconnectArguments
  45. {
  46. LocalName = localName,
  47. RemoteName = remoteName,
  48. IsDeviceMap = true
  49. });
  50. }
  51. /// <summary>Creates a connection to a network resource. The function can redirect a local device to a network resource.</summary>
  52. /// <returns>If <paramref name="localName"/> is <see langword="null"/> or <c>string.Empty</c>, returns the last available drive letter, null otherwise.</returns>
  53. /// <param name="localName">
  54. /// The name of a local device to be redirected, such as "F:". When <paramref name="localName"/> is <see langword="null"/> or
  55. /// <c>string.Empty</c>, the last available drive letter will be used. Letters are assigned beginning with Z:, then Y: and so on.
  56. /// </param>
  57. /// <param name="remoteName">The network resource to connect to. The string can be up to MAX_PATH characters in length.</param>
  58. /// <param name="userName">
  59. /// The user name for making the connection. If <paramref name="userName"/> is <see langword="null"/>, the function uses the default
  60. /// user name. (The user context for the process provides the default user name)
  61. /// </param>
  62. /// <param name="password">
  63. /// The password to be used for making the network connection. If <paramref name="password"/> is <see langword="null"/>, the function
  64. /// uses the current default password associated with the user specified by <paramref name="userName"/>.
  65. /// </param>
  66. /// <param name="prompt"><see langword="true"/> always pops-up an authentication dialog box.</param>
  67. /// <param name="updateProfile"><see langword="true"/> successful network resource connections will be saved.</param>
  68. /// <param name="saveCredentials">
  69. /// When the operating system prompts for a credential, the credential should be saved by the credential manager when true.
  70. /// </param>
  71. [SecurityCritical]
  72. public static string ConnectDrive(string localName, string remoteName, string userName, string password, bool prompt, bool updateProfile, bool saveCredentials)
  73. {
  74. return ConnectDisconnectCore(new ConnectDisconnectArguments
  75. {
  76. LocalName = localName,
  77. RemoteName = remoteName,
  78. UserName = userName,
  79. Password = password,
  80. Prompt = prompt,
  81. UpdateProfile = updateProfile,
  82. SaveCredentials = saveCredentials,
  83. IsDeviceMap = true
  84. });
  85. }
  86. /// <summary>Creates a connection to a network resource. The function can redirect a local device to a network resource.</summary>
  87. /// <returns>If <paramref name="localName"/> is <see langword="null"/> or <c>string.Empty</c>, returns the last available drive letter, null otherwise.</returns>
  88. /// <param name="localName">
  89. /// The name of a local device to be redirected, such as "F:". When <paramref name="localName"/> is <see langword="null"/> or
  90. /// <c>string.Empty</c>, the last available drive letter will be used. Letters are assigned beginning with Z:, then Y: and so on.
  91. /// </param>
  92. /// <param name="remoteName">The network resource to connect to. The string can be up to MAX_PATH characters in length.</param>
  93. /// <param name="credentials">
  94. /// An instance of <see cref="NetworkCredential"/> which provides credentials for password-based authentication schemes such as basic,
  95. /// digest, NTLM, and Kerberos authentication.
  96. /// </param>
  97. /// <param name="prompt"><see langword="true"/> always pops-up an authentication dialog box.</param>
  98. /// <param name="updateProfile"><see langword="true"/> successful network resource connections will be saved.</param>
  99. /// <param name="saveCredentials">
  100. /// When the operating system prompts for a credential, the credential should be saved by the credential manager when true.
  101. /// </param>
  102. [SecurityCritical]
  103. public static string ConnectDrive(string localName, string remoteName, NetworkCredential credentials, bool prompt, bool updateProfile, bool saveCredentials)
  104. {
  105. return ConnectDisconnectCore(new ConnectDisconnectArguments
  106. {
  107. LocalName = localName,
  108. RemoteName = remoteName,
  109. Credential = credentials,
  110. Prompt = prompt,
  111. UpdateProfile = updateProfile,
  112. SaveCredentials = saveCredentials,
  113. IsDeviceMap = true
  114. });
  115. }
  116. /// <summary>Creates a connection to a network resource. The function can redirect a local device to a network resource.</summary>
  117. /// <returns>If <paramref name="localName"/> is <see langword="null"/> or <c>string.Empty</c>, returns the last available drive letter, null otherwise.</returns>
  118. /// <param name="winOwner">Handle to a window that the provider of network resources can use as an owner window for dialog boxes.</param>
  119. /// <param name="localName">
  120. /// The name of a local device to be redirected, such as "F:". When <paramref name="localName"/> is <see langword="null"/> or
  121. /// <c>string.Empty</c>, the last available drive letter will be used. Letters are assigned beginning with Z:, then Y: and so on.
  122. /// </param>
  123. /// <param name="remoteName">The network resource to connect to. The string can be up to MAX_PATH characters in length.</param>
  124. /// <param name="userName">
  125. /// The user name for making the connection. If <paramref name="userName"/> is <see langword="null"/>, the function uses the default
  126. /// user name. (The user context for the process provides the default user name)
  127. /// </param>
  128. /// <param name="password">
  129. /// The password to be used for making the network connection. If <paramref name="password"/> is <see langword="null"/>, the function
  130. /// uses the current default password associated with the user specified by <paramref name="userName"/>.
  131. /// </param>
  132. /// <param name="prompt"><see langword="true"/> always pops-up an authentication dialog box.</param>
  133. /// <param name="updateProfile"><see langword="true"/> successful network resource connections will be saved.</param>
  134. /// <param name="saveCredentials">
  135. /// When the operating system prompts for a credential, the credential should be saved by the credential manager when true.
  136. /// </param>
  137. [SecurityCritical]
  138. public static string ConnectDrive(IntPtr winOwner, string localName, string remoteName, string userName, string password, bool prompt, bool updateProfile, bool saveCredentials)
  139. {
  140. return ConnectDisconnectCore(new ConnectDisconnectArguments
  141. {
  142. WinOwner = winOwner,
  143. LocalName = localName,
  144. RemoteName = remoteName,
  145. UserName = userName,
  146. Password = password,
  147. Prompt = prompt,
  148. UpdateProfile = updateProfile,
  149. SaveCredentials = saveCredentials,
  150. IsDeviceMap = true
  151. });
  152. }
  153. /// <summary>Creates a connection to a network resource. The function can redirect a local device to a network resource.</summary>
  154. /// <returns>If <paramref name="localName"/> is <see langword="null"/> or <c>string.Empty</c>, returns the last available drive letter, null otherwise.</returns>
  155. /// <param name="winOwner">Handle to a window that the provider of network resources can use as an owner window for dialog boxes.</param>
  156. /// <param name="localName">
  157. /// The name of a local device to be redirected, such as "F:". When <paramref name="localName"/> is <see langword="null"/> or
  158. /// <c>string.Empty</c>, the last available drive letter will be used. Letters are assigned beginning with Z:, then Y: and so on.
  159. /// </param>
  160. /// <param name="remoteName">The network resource to connect to. The string can be up to MAX_PATH characters in length.</param>
  161. /// <param name="credentials">
  162. /// An instance of <see cref="NetworkCredential"/> which provides credentials for password-based authentication schemes such as basic,
  163. /// digest, NTLM, and Kerberos authentication.
  164. /// </param>
  165. /// <param name="prompt"><see langword="true"/> always pops-up an authentication dialog box.</param>
  166. /// <param name="updateProfile"><see langword="true"/> successful network resource connections will be saved.</param>
  167. /// <param name="saveCredentials">
  168. /// When the operating system prompts for a credential, the credential should be saved by the credential manager when true.
  169. /// </param>
  170. [SecurityCritical]
  171. public static string ConnectDrive(IntPtr winOwner, string localName, string remoteName, NetworkCredential credentials, bool prompt, bool updateProfile, bool saveCredentials)
  172. {
  173. return ConnectDisconnectCore(new ConnectDisconnectArguments
  174. {
  175. WinOwner = winOwner,
  176. LocalName = localName,
  177. RemoteName = remoteName,
  178. Credential = credentials,
  179. Prompt = prompt,
  180. UpdateProfile = updateProfile,
  181. SaveCredentials = saveCredentials,
  182. IsDeviceMap = true
  183. });
  184. }
  185. #endregion // ConnectDrive
  186. #region ConnectTo
  187. /// <summary>Creates a connection to a network resource.</summary>
  188. /// <exception cref="NetworkInformationException"/>
  189. /// <param name="remoteName">A network resource to connect to, for example: \\server or \\server\share.</param>
  190. [SecurityCritical]
  191. public static void ConnectTo(string remoteName)
  192. {
  193. ConnectDisconnectCore(new ConnectDisconnectArguments { RemoteName = remoteName });
  194. }
  195. /// <summary>Creates a connection to a network resource.</summary>
  196. /// <exception cref="NetworkInformationException"/>
  197. /// <param name="remoteName">A network resource to connect to, for example: \\server or \\server\share.</param>
  198. /// <param name="userName">
  199. /// The user name for making the connection. If <paramref name="userName"/> is <see langword="null"/>, the function uses the default
  200. /// user name. (The user context for the process provides the default user name)
  201. /// </param>
  202. /// <param name="password">
  203. /// The password to be used for making the network connection. If <paramref name="password"/> is <see langword="null"/>, the function
  204. /// uses the current default password associated with the user specified by <paramref name="userName"/>.
  205. /// </param>
  206. /// <param name="prompt"><see langword="true"/> always pops-up an authentication dialog box.</param>
  207. /// <param name="updateProfile"><see langword="true"/> successful network resource connections will be saved.</param>
  208. /// <param name="saveCredentials">
  209. /// When the operating system prompts for a credential, the credential should be saved by the credential manager when true.
  210. /// </param>
  211. [SecurityCritical]
  212. public static void ConnectTo(string remoteName, string userName, string password, bool prompt, bool updateProfile, bool saveCredentials)
  213. {
  214. ConnectDisconnectCore(new ConnectDisconnectArguments
  215. {
  216. RemoteName = remoteName,
  217. UserName = userName,
  218. Password = password,
  219. Prompt = prompt,
  220. UpdateProfile = updateProfile,
  221. SaveCredentials = saveCredentials
  222. });
  223. }
  224. /// <summary>Creates a connection to a network resource.</summary>
  225. /// <param name="remoteName">A network resource to connect to, for example: \\server or \\server\share.</param>
  226. /// <param name="credentials">An instance of <see cref="NetworkCredential"/> which provides credentials for password-based authentication schemes such as basic, digest, NTLM, and Kerberos authentication.</param>
  227. /// <param name="prompt"><see langword="true"/> always pops-up an authentication dialog box.</param>
  228. /// <param name="updateProfile"><see langword="true"/> successful network resource connections will be saved.</param>
  229. /// <param name="saveCredentials">When the operating system prompts for a credential, the credential should be saved by the credential manager when true.</param>
  230. ///
  231. /// <exception cref="NetworkInformationException"/>
  232. [SecurityCritical]
  233. public static void ConnectTo(string remoteName, NetworkCredential credentials, bool prompt, bool updateProfile, bool saveCredentials)
  234. {
  235. ConnectDisconnectCore(new ConnectDisconnectArguments
  236. {
  237. RemoteName = remoteName,
  238. Credential = credentials,
  239. Prompt = prompt,
  240. UpdateProfile = updateProfile,
  241. SaveCredentials = saveCredentials
  242. });
  243. }
  244. /// <summary>Creates a connection to a network resource.</summary>
  245. /// <exception cref="NetworkInformationException"/>
  246. /// <param name="winOwner">Handle to a window that the provider of network resources can use as an owner window for dialog boxes.</param>
  247. /// <param name="remoteName">A network resource to connect to, for example: \\server or \\server\share.</param>
  248. /// <param name="userName">
  249. /// The user name for making the connection. If <paramref name="userName"/> is <see langword="null"/>, the function uses the default
  250. /// user name. (The user context for the process provides the default user name)
  251. /// </param>
  252. /// <param name="password">
  253. /// The password to be used for making the network connection. If <paramref name="password"/> is <see langword="null"/>, the function
  254. /// uses the current default password associated with the user specified by <paramref name="userName"/>.
  255. /// </param>
  256. /// <param name="prompt"><see langword="true"/> always pops-up an authentication dialog box.</param>
  257. /// <param name="updateProfile"><see langword="true"/> successful network resource connections will be saved.</param>
  258. /// <param name="saveCredentials">When the operating system prompts for a credential, the credential should be saved by the credential manager when true.</param>
  259. [SecurityCritical]
  260. public static void ConnectTo(IntPtr winOwner, string remoteName, string userName, string password, bool prompt, bool updateProfile, bool saveCredentials)
  261. {
  262. ConnectDisconnectCore(new ConnectDisconnectArguments
  263. {
  264. WinOwner = winOwner,
  265. RemoteName = remoteName,
  266. UserName = userName,
  267. Password = password,
  268. Prompt = prompt,
  269. UpdateProfile = updateProfile,
  270. SaveCredentials = saveCredentials
  271. });
  272. }
  273. /// <summary>Creates a connection to a network resource.</summary>
  274. /// <exception cref="NetworkInformationException"/>
  275. /// <param name="winOwner">Handle to a window that the provider of network resources can use as an owner window for dialog boxes.</param>
  276. /// <param name="remoteName">A network resource to connect to, for example: \\server or \\server\share.</param>
  277. /// <param name="credentials">An instance of <see cref="NetworkCredential"/> which provides credentials for password-based authentication schemes such as basic, digest, NTLM, and Kerberos authentication.</param>
  278. /// <param name="prompt"><see langword="true"/> always pops-up an authentication dialog box.</param>
  279. /// <param name="updateProfile"><see langword="true"/> successful network resource connections will be saved.</param>
  280. /// <param name="saveCredentials">When the operating system prompts for a credential, the credential should be saved by the credential manager when true.</param>
  281. [SecurityCritical]
  282. public static void ConnectTo(IntPtr winOwner, string remoteName, NetworkCredential credentials, bool prompt, bool updateProfile, bool saveCredentials)
  283. {
  284. ConnectDisconnectCore(new ConnectDisconnectArguments
  285. {
  286. WinOwner = winOwner,
  287. RemoteName = remoteName,
  288. Credential = credentials,
  289. Prompt = prompt,
  290. UpdateProfile = updateProfile,
  291. SaveCredentials = saveCredentials
  292. });
  293. }
  294. #endregion // ConnectTo
  295. #region DisconnectDrive
  296. /// <summary>Cancels an existing network connection. You can also call the function to remove remembered network connections that are not currently connected.</summary>
  297. /// <param name="localName">The name of a local device to be disconnected, such as "F:".</param>
  298. [SecurityCritical]
  299. public static void DisconnectDrive(string localName)
  300. {
  301. ConnectDisconnectCore(new ConnectDisconnectArguments
  302. {
  303. LocalName = localName,
  304. IsDeviceMap = true,
  305. IsDisconnect = true
  306. });
  307. }
  308. /// <summary>Cancels an existing network connection. You can also call the function to remove remembered network connections that are not currently connected.</summary>
  309. /// <param name="localName">The name of a local device to be disconnected, such as "F:".</param>
  310. /// <param name="force">
  311. /// Specifies whether the disconnection should occur if there are open files or jobs on the connection.
  312. /// If this parameter is <see langword="false"/>, the function fails if there are open files or jobs.
  313. /// </param>
  314. /// <param name="updateProfile"><see langword="true"/> successful removal of network resource connections will be saved.</param>
  315. [SecurityCritical]
  316. public static void DisconnectDrive(string localName, bool force, bool updateProfile)
  317. {
  318. ConnectDisconnectCore(new ConnectDisconnectArguments
  319. {
  320. LocalName = localName,
  321. Prompt = force,
  322. UpdateProfile = updateProfile,
  323. IsDeviceMap = true,
  324. IsDisconnect = true
  325. });
  326. }
  327. #endregion // DisconnectDrive
  328. #region DisconnectFrom
  329. /// <summary>Cancels an existing network connection. You can also call the function to remove remembered network connections that are not currently connected.</summary>
  330. /// <param name="remoteName">A network resource to disconnect from, for example: \\server or \\server\share.</param>
  331. [SecurityCritical]
  332. public static void DisconnectFrom(string remoteName)
  333. {
  334. ConnectDisconnectCore(new ConnectDisconnectArguments
  335. {
  336. RemoteName = remoteName,
  337. IsDisconnect = true
  338. });
  339. }
  340. /// <summary>Cancels an existing network connection. You can also call the function to remove remembered network connections that are not currently connected.</summary>
  341. /// <param name="remoteName">A network resource to disconnect from, for example: \\server or \\server\share.</param>
  342. /// <param name="force">
  343. /// Specifies whether the disconnection should occur if there are open files or jobs on the connection.
  344. /// If this parameter is <see langword="false"/>, the function fails if there are open files or jobs.
  345. /// </param>
  346. /// <param name="updateProfile"><see langword="true"/> successful removal of network resource connections will be saved.</param>
  347. [SecurityCritical]
  348. public static void DisconnectFrom(string remoteName, bool force, bool updateProfile)
  349. {
  350. ConnectDisconnectCore(new ConnectDisconnectArguments
  351. {
  352. RemoteName = remoteName,
  353. Prompt = force,
  354. UpdateProfile = updateProfile,
  355. IsDisconnect = true
  356. });
  357. }
  358. #endregion // DisconnectFrom
  359. #region Internal Methods
  360. /// <summary>Connects to/disconnects from a network resource. The function can redirect a local device to a network resource.</summary>
  361. /// <returns>If <see cref="ConnectDisconnectArguments.LocalName"/> is <see langword="null"/> or <c>string.Empty</c>, returns the last available drive letter, null otherwise.</returns>
  362. /// <exception cref="ArgumentNullException"/>
  363. /// <exception cref="NetworkInformationException"/>
  364. /// <param name="arguments">The <see cref="ConnectDisconnectArguments"/>.</param>
  365. [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly")]
  366. [SecurityCritical]
  367. internal static string ConnectDisconnectCore(ConnectDisconnectArguments arguments)
  368. {
  369. uint lastError;
  370. // Always remove backslash for local device.
  371. if (!Utils.IsNullOrWhiteSpace(arguments.LocalName))
  372. arguments.LocalName = Path.RemoveTrailingDirectorySeparator(arguments.LocalName, false).ToUpperInvariant();
  373. #region Disconnect
  374. if (arguments.IsDisconnect)
  375. {
  376. bool force = arguments.Prompt; // Use value of prompt variable for force value.
  377. string target = arguments.IsDeviceMap ? arguments.LocalName : arguments.RemoteName;
  378. if (Utils.IsNullOrWhiteSpace(target))
  379. throw new ArgumentNullException(arguments.IsDeviceMap ? "localName" : "remoteName");
  380. lastError = NativeMethods.WNetCancelConnection(target, arguments.UpdateProfile ? NativeMethods.Connect.UpdateProfile : NativeMethods.Connect.None, force);
  381. if (lastError != Win32Errors.NO_ERROR)
  382. throw new NetworkInformationException((int)lastError);
  383. return null;
  384. }
  385. #endregion // Disconnect
  386. #region Connect
  387. // arguments.LocalName is allowed to be null or empty.
  388. //if (Utils.IsNullOrWhiteSpace(arguments.LocalName) && !arguments.IsDeviceMap)
  389. // throw new ArgumentNullException("localName");
  390. if (Utils.IsNullOrWhiteSpace(arguments.RemoteName) && !arguments.IsDeviceMap)
  391. throw new ArgumentNullException("remoteName");
  392. // When supplied, use data from NetworkCredential instance.
  393. if (arguments.Credential != null)
  394. {
  395. arguments.UserName = Utils.IsNullOrWhiteSpace(arguments.Credential.Domain)
  396. ? arguments.Credential.UserName
  397. : string.Format(CultureInfo.InvariantCulture, @"{0}\{1}", arguments.Credential.Domain, arguments.Credential.UserName);
  398. arguments.Password = arguments.Credential.Password;
  399. }
  400. // Assemble Connect arguments.
  401. var connect = NativeMethods.Connect.None;
  402. if (arguments.IsDeviceMap)
  403. connect = connect | NativeMethods.Connect.Redirect;
  404. if (arguments.Prompt)
  405. connect = connect | NativeMethods.Connect.Prompt | NativeMethods.Connect.Interactive;
  406. if (arguments.UpdateProfile)
  407. connect = connect | NativeMethods.Connect.UpdateProfile;
  408. if (arguments.SaveCredentials)
  409. connect = connect | NativeMethods.Connect.SaveCredentialManager;
  410. // Initialize structure.
  411. var resource = new NativeMethods.NETRESOURCE
  412. {
  413. lpLocalName = arguments.LocalName,
  414. lpRemoteName = arguments.RemoteName,
  415. dwType = NativeMethods.ResourceType.Disk
  416. };
  417. // Three characters for: "X:\0" (Drive X: with null terminator)
  418. uint bufferSize = 3;
  419. StringBuilder buffer;
  420. do
  421. {
  422. buffer = new StringBuilder((int)bufferSize);
  423. uint result;
  424. lastError = NativeMethods.WNetUseConnection(arguments.WinOwner, ref resource, arguments.Password, arguments.UserName, connect, buffer, out bufferSize, out result);
  425. switch (lastError)
  426. {
  427. case Win32Errors.NO_ERROR:
  428. break;
  429. case Win32Errors.ERROR_MORE_DATA:
  430. // MSDN, lpBufferSize: If the call fails because the buffer is not large enough,
  431. // the function returns the required buffer size in this location.
  432. //
  433. // Windows 8 x64: bufferSize remains unchanged.
  434. bufferSize = bufferSize * 2;
  435. break;
  436. }
  437. } while (lastError == Win32Errors.ERROR_MORE_DATA);
  438. if (lastError != Win32Errors.NO_ERROR)
  439. throw new NetworkInformationException((int)lastError);
  440. return arguments.IsDeviceMap ? buffer.ToString() : null;
  441. #endregion // Connect
  442. }
  443. #endregion // Internal Methods
  444. }
  445. }