Stream.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766
  1. <?php
  2. /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
  3. /**
  4. * SFTP Stream Wrapper
  5. *
  6. * Creates an sftp:// protocol handler that can be used with, for example, fopen(), dir(), etc.
  7. *
  8. * PHP version 5
  9. *
  10. * LICENSE: Permission is hereby granted, free of charge, to any person obtaining a copy
  11. * of this software and associated documentation files (the "Software"), to deal
  12. * in the Software without restriction, including without limitation the rights
  13. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  14. * copies of the Software, and to permit persons to whom the Software is
  15. * furnished to do so, subject to the following conditions:
  16. *
  17. * The above copyright notice and this permission notice shall be included in
  18. * all copies or substantial portions of the Software.
  19. *
  20. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  21. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  22. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  23. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  24. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  25. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  26. * THE SOFTWARE.
  27. *
  28. * @category Net
  29. * @package Net_SFTP_Stream
  30. * @author Jim Wigginton <terrafrost@php.net>
  31. * @copyright MMXIII Jim Wigginton
  32. * @license http://www.opensource.org/licenses/mit-license.html MIT License
  33. * @link http://phpseclib.sourceforge.net
  34. */
  35. /**
  36. * SFTP Stream Wrapper
  37. *
  38. * @author Jim Wigginton <terrafrost@php.net>
  39. * @version 0.3.2
  40. * @access public
  41. * @package Net_SFTP_Stream
  42. */
  43. class Net_SFTP_Stream {
  44. /**
  45. * SFTP instances
  46. *
  47. * Rather than re-create the connection we re-use instances if possible
  48. *
  49. * @var Array
  50. * @access static
  51. */
  52. static $instances;
  53. /**
  54. * SFTP instance
  55. *
  56. * @var Object
  57. * @access private
  58. */
  59. var $sftp;
  60. /**
  61. * Path
  62. *
  63. * @var String
  64. * @access private
  65. */
  66. var $path;
  67. /**
  68. * Mode
  69. *
  70. * @var String
  71. * @access private
  72. */
  73. var $mode;
  74. /**
  75. * Position
  76. *
  77. * @var Integer
  78. * @access private
  79. */
  80. var $pos;
  81. /**
  82. * Size
  83. *
  84. * @var Integer
  85. * @access private
  86. */
  87. var $size;
  88. /**
  89. * Directory entries
  90. *
  91. * @var Array
  92. * @access private
  93. */
  94. var $entries;
  95. /**
  96. * EOF flag
  97. *
  98. * @var Boolean
  99. * @access private
  100. */
  101. var $eof;
  102. /**
  103. * Context resource
  104. *
  105. * Technically this needs to be publically accessible so PHP can set it directly
  106. *
  107. * @var Resource
  108. * @access public
  109. */
  110. var $context;
  111. /**
  112. * Notification callback function
  113. *
  114. * @var Callable
  115. * @access public
  116. */
  117. var $notification;
  118. /**
  119. * The Constructor
  120. *
  121. * @access public
  122. */
  123. function Net_SFTP_Stream()
  124. {
  125. if (!class_exists('Net_SFTP')) {
  126. require_once('Net/SFTP.php');
  127. }
  128. }
  129. /**
  130. * Path Parser
  131. *
  132. * Extract a path from a URI and actually connect to an SSH server if appropriate
  133. *
  134. * If "notification" is set as a context parameter the message code for successful login is
  135. * NET_SSH2_MSG_USERAUTH_SUCCESS. For a failed login it's NET_SSH2_MSG_USERAUTH_FAILURE.
  136. *
  137. * @param String $path
  138. * @return String
  139. * @access private
  140. */
  141. function _parse_path($path)
  142. {
  143. extract(parse_url($path));
  144. if (!isset($host)) {
  145. return false;
  146. }
  147. $context = stream_context_get_params($this->context);
  148. if (isset($context['notification'])) {
  149. $this->notification = $context['notification'];
  150. }
  151. if ($host[0] == '$') {
  152. $host = substr($host, 1);
  153. global $$host;
  154. if (!is_object($$host) || get_class($$host) != 'Net_sFTP') {
  155. return false;
  156. }
  157. $this->sftp = $$host;
  158. } else {
  159. $context = stream_context_get_options($this->context);
  160. if (isset($context['sftp']['session'])) {
  161. $sftp = $context['sftp']['session'];
  162. }
  163. if (isset($context['sftp']['sftp'])) {
  164. $sftp = $context['sftp']['sftp'];
  165. }
  166. if (isset($sftp) && is_object($sftp) && get_class($sftp) == 'Net_SFTP') {
  167. $this->sftp = $sftp;
  168. return $path;
  169. }
  170. if (isset($context['sftp']['username'])) {
  171. $user = $context['sftp']['username'];
  172. }
  173. if (isset($context['sftp']['password'])) {
  174. $pass = $context['sftp']['password'];
  175. }
  176. if (isset($context['sftp']['privkey']) && is_object($context['sftp']['privkey']) && get_Class($context['sftp']['privkey']) == 'Crypt_RSA') {
  177. $pass = $context['sftp']['privkey'];
  178. }
  179. if (!isset($user) || !isset($pass)) {
  180. return false;
  181. }
  182. // casting $pass to a string is necessary in the event that it's a Crypt_RSA object
  183. if (isset(self::$instances[$host][$port][$user][(string) $pass])) {
  184. $this->sftp = self::$instances[$host][$port][$user][(string) $pass];
  185. } else {
  186. $this->sftp = new Net_SFTP($host, isset($port) ? $port : 22);
  187. if (isset($this->notification) && is_callable($this->notification)) {
  188. /* if !is_callable($this->notification) we could do this:
  189. user_error('fopen(): failed to call user notifier', E_USER_WARNING);
  190. the ftp wrapper gives errors like that when the notifier isn't callable.
  191. i've opted not to do that, however, since the ftp wrapper gives the line
  192. on which the fopen occurred as the line number - not the line that the
  193. user_error is on.
  194. */
  195. call_user_func($this->notification, STREAM_NOTIFY_CONNECT, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0);
  196. call_user_func($this->notification, STREAM_NOTIFY_AUTH_REQUIRED, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0);
  197. if (!$this->sftp->login($user, $pass)) {
  198. call_user_func($this->notification, STREAM_NOTIFY_AUTH_RESULT, STREAM_NOTIFY_SEVERITY_ERR, 'Login Failure', NET_SSH2_MSG_USERAUTH_FAILURE, 0, 0);
  199. return false;
  200. }
  201. call_user_func($this->notification, STREAM_NOTIFY_AUTH_RESULT, STREAM_NOTIFY_SEVERITY_INFO, 'Login Success', NET_SSH2_MSG_USERAUTH_SUCCESS, 0, 0);
  202. } else {
  203. if (!$this->sftp->login($user, $pass)) {
  204. return false;
  205. }
  206. }
  207. self::$instances[$host][$port][$user][(string) $pass] = $this->sftp;
  208. }
  209. }
  210. return $path;
  211. }
  212. /**
  213. * Opens file or URL
  214. *
  215. * @param String $path
  216. * @param String $mode
  217. * @param Integer $options
  218. * @param String $opened_path
  219. * @return Boolean
  220. * @access public
  221. */
  222. function _stream_open($path, $mode, $options, &$opened_path)
  223. {
  224. $path = $this->_parse_path($path);
  225. if ($path === false) {
  226. return false;
  227. }
  228. $this->path = $path;
  229. $this->size = $this->sftp->size($path);
  230. $this->mode = preg_replace('#[bt]$#', '', $mode);
  231. if ($this->size === false) {
  232. if ($this->mode[0] == 'r') {
  233. return false;
  234. }
  235. } else {
  236. switch ($this->mode[0]) {
  237. case 'x':
  238. return false;
  239. case 'w':
  240. case 'c':
  241. $this->sftp->truncate($path, 0);
  242. }
  243. }
  244. $this->pos = $this->mode[0] != 'a' ? 0 : $this->size;
  245. return true;
  246. }
  247. /**
  248. * Read from stream
  249. *
  250. * @param Integer $count
  251. * @return Mixed
  252. * @access public
  253. */
  254. function _stream_read($count)
  255. {
  256. switch ($this->mode) {
  257. case 'w':
  258. case 'a':
  259. case 'x':
  260. case 'c':
  261. return false;
  262. }
  263. // commented out because some files - eg. /dev/urandom - will say their size is 0 when in fact it's kinda infinite
  264. //if ($this->pos >= $this->size) {
  265. // $this->eof = true;
  266. // return false;
  267. //}
  268. $result = $this->sftp->get($this->path, false, $this->pos, $count);
  269. if (isset($this->notification) && is_callable($this->notification)) {
  270. if ($result === false) {
  271. call_user_func($this->notification, STREAM_NOTIFY_FAILURE, STREAM_NOTIFY_SEVERITY_ERR, $this->sftp->getLastSFTPError(), NET_SFTP_OPEN, 0, 0);
  272. return 0;
  273. }
  274. // seems that PHP calls stream_read in 8k chunks
  275. call_user_func($this->notification, STREAM_NOTIFY_PROGRESS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, strlen($result), $this->size);
  276. }
  277. if (empty($result)) { // ie. false or empty string
  278. $this->eof = true;
  279. return false;
  280. }
  281. $this->pos+= strlen($result);
  282. return $result;
  283. }
  284. /**
  285. * Write to stream
  286. *
  287. * @param String $data
  288. * @return Mixed
  289. * @access public
  290. */
  291. function _stream_write($data)
  292. {
  293. switch ($this->mode) {
  294. case 'r':
  295. return false;
  296. }
  297. $result = $this->sftp->put($this->path, $data, NET_SFTP_STRING, $this->pos);
  298. if (isset($this->notification) && is_callable($this->notification)) {
  299. if (!$result) {
  300. call_user_func($this->notification, STREAM_NOTIFY_FAILURE, STREAM_NOTIFY_SEVERITY_ERR, $this->sftp->getLastSFTPError(), NET_SFTP_OPEN, 0, 0);
  301. return 0;
  302. }
  303. // seems that PHP splits up strings into 8k blocks before calling stream_write
  304. call_user_func($this->notification, STREAM_NOTIFY_PROGRESS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, strlen($data), strlen($data));
  305. }
  306. if ($result === false) {
  307. return false;
  308. }
  309. $this->pos+= strlen($data);
  310. if ($this->pos > $this->size) {
  311. $this->size = $this->pos;
  312. }
  313. $this->eof = false;
  314. return strlen($data);
  315. }
  316. /**
  317. * Retrieve the current position of a stream
  318. *
  319. * @return Integer
  320. * @access public
  321. */
  322. function _stream_tell()
  323. {
  324. return $this->pos;
  325. }
  326. /**
  327. * Tests for end-of-file on a file pointer
  328. *
  329. * In my testing there are four classes functions that normally effect the pointer:
  330. * fseek, fputs / fwrite, fgets / fread and ftruncate.
  331. *
  332. * Only fgets / fread, however, results in feof() returning true. do fputs($fp, 'aaa') on a blank file and feof()
  333. * will return false. do fread($fp, 1) and feof() will then return true. do fseek($fp, 10) on ablank file and feof()
  334. * will return false. do fread($fp, 1) and feof() will then return true.
  335. *
  336. * @return Boolean
  337. * @access public
  338. */
  339. function _stream_eof()
  340. {
  341. return $this->eof;
  342. }
  343. /**
  344. * Seeks to specific location in a stream
  345. *
  346. * @param Integer $offset
  347. * @param Integer $whence
  348. * @return Boolean
  349. * @access public
  350. */
  351. function _stream_seek($offset, $whence)
  352. {
  353. switch ($whence) {
  354. case SEEK_SET:
  355. if ($offset >= $this->size || $offset < 0) {
  356. return false;
  357. }
  358. break;
  359. case SEEK_CUR:
  360. $offset+= $this->pos;
  361. break;
  362. case SEEK_END:
  363. $offset+= $this->size;
  364. }
  365. $this->pos = $offset;
  366. $this->eof = false;
  367. return true;
  368. }
  369. /**
  370. * Change stream options
  371. *
  372. * @param String $path
  373. * @param Integer $option
  374. * @param Mixed $var
  375. * @return Boolean
  376. * @access public
  377. */
  378. function _stream_metadata($path, $option, $var)
  379. {
  380. $path = $this->_parse_path($path);
  381. if ($path === false) {
  382. return false;
  383. }
  384. // stream_metadata was introduced in PHP 5.4.0 but as of 5.4.11 the constants haven't been defined
  385. // see http://www.php.net/streamwrapper.stream-metadata and https://bugs.php.net/64246
  386. // and https://github.com/php/php-src/blob/master/main/php_streams.h#L592
  387. switch ($option) {
  388. case 1: // PHP_STREAM_META_TOUCH
  389. return $this->sftp->touch($path, $var[0], $var[1]);
  390. case 2: // PHP_STREAM_OWNER_NAME
  391. case 3: // PHP_STREAM_GROUP_NAME
  392. return false;
  393. case 4: // PHP_STREAM_META_OWNER
  394. return $this->sftp->chown($path, $var);
  395. case 5: // PHP_STREAM_META_GROUP
  396. return $this->sftp->chgrp($path, $var);
  397. case 6: // PHP_STREAM_META_ACCESS
  398. return $this->sftp->chmod($path, $var) !== false;
  399. }
  400. }
  401. /**
  402. * Retrieve the underlaying resource
  403. *
  404. * @param Integer $cast_as
  405. * @return Resource
  406. * @access public
  407. */
  408. function _stream_cast($cast_as)
  409. {
  410. return $this->sftp->fsock;
  411. }
  412. /**
  413. * Advisory file locking
  414. *
  415. * @param Integer $operation
  416. * @return Boolean
  417. * @access public
  418. */
  419. function _stream_lock($operation)
  420. {
  421. return false;
  422. }
  423. /**
  424. * Renames a file or directory
  425. *
  426. * Attempts to rename oldname to newname, moving it between directories if necessary.
  427. * If newname exists, it will be overwritten. This is a departure from what Net_SFTP
  428. * does.
  429. *
  430. * @param String $path_from
  431. * @param String $path_to
  432. * @return Boolean
  433. * @access public
  434. */
  435. function _rename($path_from, $path_to)
  436. {
  437. $path1 = parse_url($path_from);
  438. $path2 = parse_url($path_to);
  439. unset($path1['path'], $path2['path']);
  440. if ($path1 != $path2) {
  441. return false;
  442. }
  443. $path_from = $this->_parse_path($path_from);
  444. $path_to = parse_url($path_to);
  445. if ($path_from == false) {
  446. return false;
  447. }
  448. $path_to = $path_to['path']; // the $component part of parse_url() was added in PHP 5.1.2
  449. // "It is an error if there already exists a file with the name specified by newpath."
  450. // -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.5
  451. if (!$this->sftp->rename($path_from, $path_to)) {
  452. if ($this->sftp->stat($path_to)) {
  453. return $this->sftp->delete($path_to, true) && $this->sftp->rename($path_from, $path_to);
  454. }
  455. return false;
  456. }
  457. return true;
  458. }
  459. /**
  460. * Open directory handle
  461. *
  462. * The only $options is "whether or not to enforce safe_mode (0x04)". Since safe mode was deprecated in 5.3 and
  463. * removed in 5.4 I'm just going to ignore it
  464. *
  465. * @param String $path
  466. * @param Integer $options
  467. * @return Boolean
  468. * @access public
  469. */
  470. function _dir_opendir($path, $options)
  471. {
  472. $path = $this->_parse_path($path);
  473. if ($path === false) {
  474. return false;
  475. }
  476. $this->pos = 0;
  477. $this->entries = $this->sftp->nlist($path);
  478. return $this->entries !== false;
  479. }
  480. /**
  481. * Read entry from directory handle
  482. *
  483. * @return Mixed
  484. * @access public
  485. */
  486. function _dir_readdir()
  487. {
  488. if (isset($this->entries[$this->pos])) {
  489. return $this->entries[$this->pos++];
  490. }
  491. return false;
  492. }
  493. /**
  494. * Rewind directory handle
  495. *
  496. * @return Boolean
  497. * @access public
  498. */
  499. function _dir_rewinddir()
  500. {
  501. $this->pos = 0;
  502. return true;
  503. }
  504. /**
  505. * Close directory handle
  506. *
  507. * @return Boolean
  508. * @access public
  509. */
  510. function _dir_closedir()
  511. {
  512. return true;
  513. }
  514. /**
  515. * Create a directory
  516. *
  517. * Only valid $options is STREAM_MKDIR_RECURSIVE
  518. *
  519. * @param String $path
  520. * @param Integer $mode
  521. * @param Integer $options
  522. * @return Boolean
  523. * @access public
  524. */
  525. function _mkdir($path, $mode, $options)
  526. {
  527. $path = $this->_parse_path($path);
  528. if ($path === false) {
  529. return false;
  530. }
  531. return $this->sftp->mkdir($path, $mode, $options & STREAM_MKDIR_RECURSIVE);
  532. }
  533. /**
  534. * Removes a directory
  535. *
  536. * Only valid $options is STREAM_MKDIR_RECURSIVE per <http://php.net/streamwrapper.rmdir>, however,
  537. * <http://php.net/rmdir> does not have a $recursive parameter as mkdir() does so I don't know how
  538. * STREAM_MKDIR_RECURSIVE is supposed to be set. Also, when I try it out with rmdir() I get 8 as
  539. * $options. What does 8 correspond to?
  540. *
  541. * @param String $path
  542. * @param Integer $mode
  543. * @param Integer $options
  544. * @return Boolean
  545. * @access public
  546. */
  547. function _rmdir($path, $options)
  548. {
  549. $path = $this->_parse_path($path);
  550. if ($path === false) {
  551. return false;
  552. }
  553. return $this->sftp->rmdir($path);
  554. }
  555. /**
  556. * Flushes the output
  557. *
  558. * See <http://php.net/fflush>. Always returns true because Net_SFTP doesn't cache stuff before writing
  559. *
  560. * @return Boolean
  561. * @access public
  562. */
  563. function _stream_flush()
  564. {
  565. return true;
  566. }
  567. /**
  568. * Retrieve information about a file resource
  569. *
  570. * @return Mixed
  571. * @access public
  572. */
  573. function _stream_stat()
  574. {
  575. $results = $this->sftp->stat($this->path);
  576. if ($results === false) {
  577. return false;
  578. }
  579. return $results;
  580. }
  581. /**
  582. * Delete a file
  583. *
  584. * @param String $path
  585. * @return Boolean
  586. * @access public
  587. */
  588. function _unlink($path)
  589. {
  590. $path = $this->_parse_path($path);
  591. if ($path === false) {
  592. return false;
  593. }
  594. return $this->sftp->delete($path, false);
  595. }
  596. /**
  597. * Retrieve information about a file
  598. *
  599. * Ignores the STREAM_URL_STAT_QUIET flag because the entirety of Net_SFTP_Stream is quiet by default
  600. * might be worthwhile to reconstruct bits 12-16 (ie. the file type) if mode doesn't have them but we'll
  601. * cross that bridge when and if it's reached
  602. *
  603. * @param String $path
  604. * @param Integer $flags
  605. * @return Mixed
  606. * @access public
  607. */
  608. function _url_stat($path, $flags)
  609. {
  610. $path = $this->_parse_path($path);
  611. if ($path === false) {
  612. return false;
  613. }
  614. $results = $flags & STREAM_URL_STAT_LINK ? $this->sftp->lstat($path) : $this->sftp->stat($path);
  615. if ($results === false) {
  616. return false;
  617. }
  618. return $results;
  619. }
  620. /**
  621. * Truncate stream
  622. *
  623. * @param Integer $new_size
  624. * @return Boolean
  625. * @access public
  626. */
  627. function _stream_truncate($new_size)
  628. {
  629. if (!$this->sftp->truncate($this->path, $new_size)) {
  630. return false;
  631. }
  632. $this->eof = false;
  633. $this->size = $new_size;
  634. return true;
  635. }
  636. /**
  637. * Change stream options
  638. *
  639. * STREAM_OPTION_WRITE_BUFFER isn't supported for the same reason stream_flush isn't.
  640. * The other two aren't supported because of limitations in Net_SFTP.
  641. *
  642. * @param Integer $option
  643. * @param Integer $arg1
  644. * @param Integer $arg2
  645. * @return Boolean
  646. * @access public
  647. */
  648. function _stream_set_option($option, $arg1, $arg2)
  649. {
  650. return false;
  651. }
  652. /**
  653. * Close an resource
  654. *
  655. * @access public
  656. */
  657. function _stream_close()
  658. {
  659. }
  660. /**
  661. * __call Magic Method
  662. *
  663. * When you're utilizing an SFTP stream you're not calling the methods in this class directly - PHP is calling them for you.
  664. * Which kinda begs the question... what methods is PHP calling and what parameters is it passing to them? This function
  665. * lets you figure that out.
  666. *
  667. * If NET_SFTP_STREAM_LOGGING is defined all calls will be output on the screen and then (regardless of whether or not
  668. * NET_SFTP_STREAM_LOGGING is enabled) the parameters will be passed through to the appropriate method.
  669. *
  670. * @param String
  671. * @param Array
  672. * @return Mixed
  673. * @access public
  674. */
  675. function __call($name, $arguments)
  676. {
  677. if (defined('NET_SFTP_STREAM_LOGGING')) {
  678. echo $name . '(';
  679. $last = count($arguments) - 1;
  680. foreach ($arguments as $i => $argument) {
  681. var_export($argument);
  682. if ($i != $last) {
  683. echo ',';
  684. }
  685. }
  686. echo ")\r\n";
  687. }
  688. $name = '_' . $name;
  689. if (!method_exists($this, $name)) {
  690. return false;
  691. }
  692. return call_user_func_array(array($this, $name), $arguments);
  693. }
  694. }
  695. if (function_exists('stream_wrapper_register')) {
  696. stream_wrapper_register('sftp', 'Net_SFTP_Stream');
  697. }