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.

638 lines
18 KiB

12 years ago
21 years ago
20 years ago
21 years ago
21 years ago
18 years ago
18 years ago
18 years ago
  1. /*
  2. +----------------------------------------------------------------------+
  3. | PHP Version 5 |
  4. +----------------------------------------------------------------------+
  5. | Copyright (c) 1997-2014 The PHP Group |
  6. +----------------------------------------------------------------------+
  7. | This source file is subject to version 3.01 of the PHP license, |
  8. | that is bundled with this package in the file LICENSE, and is |
  9. | available through the world-wide-web at the following url: |
  10. | http://www.php.net/license/3_01.txt |
  11. | If you did not receive a copy of the PHP license and are unable to |
  12. | obtain it through the world-wide-web, please send a note to |
  13. | license@php.net so we can mail you a copy immediately. |
  14. +----------------------------------------------------------------------+
  15. | Authors: Edin Kadribasic <edink@emini.dk> |
  16. | Ilia Alshanestsky <ilia@prohost.org> |
  17. | Wez Furlong <wez@php.net> |
  18. +----------------------------------------------------------------------+
  19. */
  20. /* $Id$ */
  21. #ifdef HAVE_CONFIG_H
  22. #include "config.h"
  23. #endif
  24. #include "php.h"
  25. #include "php_ini.h"
  26. #include "ext/standard/info.h"
  27. #include "pdo/php_pdo.h"
  28. #include "pdo/php_pdo_driver.h"
  29. #include "php_pdo_pgsql.h"
  30. #include "php_pdo_pgsql_int.h"
  31. #if HAVE_NETINET_IN_H
  32. #include <netinet/in.h>
  33. #endif
  34. /* from postgresql/src/include/catalog/pg_type.h */
  35. #define BOOLOID 16
  36. #define BYTEAOID 17
  37. #define INT8OID 20
  38. #define INT2OID 21
  39. #define INT4OID 23
  40. #define TEXTOID 25
  41. #define OIDOID 26
  42. static int pgsql_stmt_dtor(pdo_stmt_t *stmt TSRMLS_DC)
  43. {
  44. pdo_pgsql_stmt *S = (pdo_pgsql_stmt*)stmt->driver_data;
  45. if (S->result) {
  46. /* free the resource */
  47. PQclear(S->result);
  48. S->result = NULL;
  49. }
  50. if (S->stmt_name) {
  51. pdo_pgsql_db_handle *H = S->H;
  52. char *q = NULL;
  53. PGresult *res;
  54. if (S->is_prepared) {
  55. spprintf(&q, 0, "DEALLOCATE %s", S->stmt_name);
  56. res = PQexec(H->server, q);
  57. efree(q);
  58. if (res) {
  59. PQclear(res);
  60. }
  61. }
  62. efree(S->stmt_name);
  63. S->stmt_name = NULL;
  64. }
  65. if (S->param_lengths) {
  66. efree(S->param_lengths);
  67. S->param_lengths = NULL;
  68. }
  69. if (S->param_values) {
  70. efree(S->param_values);
  71. S->param_values = NULL;
  72. }
  73. if (S->param_formats) {
  74. efree(S->param_formats);
  75. S->param_formats = NULL;
  76. }
  77. if (S->param_types) {
  78. efree(S->param_types);
  79. S->param_types = NULL;
  80. }
  81. if (S->query) {
  82. efree(S->query);
  83. S->query = NULL;
  84. }
  85. if (S->cursor_name) {
  86. pdo_pgsql_db_handle *H = S->H;
  87. char *q = NULL;
  88. PGresult *res;
  89. spprintf(&q, 0, "CLOSE %s", S->cursor_name);
  90. res = PQexec(H->server, q);
  91. efree(q);
  92. if (res) PQclear(res);
  93. efree(S->cursor_name);
  94. S->cursor_name = NULL;
  95. }
  96. if(S->cols) {
  97. efree(S->cols);
  98. S->cols = NULL;
  99. }
  100. efree(S);
  101. stmt->driver_data = NULL;
  102. return 1;
  103. }
  104. static int pgsql_stmt_execute(pdo_stmt_t *stmt TSRMLS_DC)
  105. {
  106. pdo_pgsql_stmt *S = (pdo_pgsql_stmt*)stmt->driver_data;
  107. pdo_pgsql_db_handle *H = S->H;
  108. ExecStatusType status;
  109. /* ensure that we free any previous unfetched results */
  110. if(S->result) {
  111. PQclear(S->result);
  112. S->result = NULL;
  113. }
  114. S->current_row = 0;
  115. if (S->cursor_name) {
  116. char *q = NULL;
  117. if (S->is_prepared) {
  118. spprintf(&q, 0, "CLOSE %s", S->cursor_name);
  119. S->result = PQexec(H->server, q);
  120. efree(q);
  121. }
  122. spprintf(&q, 0, "DECLARE %s SCROLL CURSOR WITH HOLD FOR %s", S->cursor_name, stmt->active_query_string);
  123. S->result = PQexec(H->server, q);
  124. efree(q);
  125. /* check if declare failed */
  126. status = PQresultStatus(S->result);
  127. if (status != PGRES_COMMAND_OK && status != PGRES_TUPLES_OK) {
  128. pdo_pgsql_error_stmt(stmt, status, pdo_pgsql_sqlstate(S->result));
  129. return 0;
  130. }
  131. /* the cursor was declared correctly */
  132. S->is_prepared = 1;
  133. /* fetch to be able to get the number of tuples later, but don't advance the cursor pointer */
  134. spprintf(&q, 0, "FETCH FORWARD 0 FROM %s", S->cursor_name);
  135. S->result = PQexec(H->server, q);
  136. efree(q);
  137. } else if (S->stmt_name) {
  138. /* using a prepared statement */
  139. if (!S->is_prepared) {
  140. stmt_retry:
  141. /* we deferred the prepare until now, because we didn't
  142. * know anything about the parameter types; now we do */
  143. S->result = PQprepare(H->server, S->stmt_name, S->query,
  144. stmt->bound_params ? zend_hash_num_elements(stmt->bound_params) : 0,
  145. S->param_types);
  146. status = PQresultStatus(S->result);
  147. switch (status) {
  148. case PGRES_COMMAND_OK:
  149. case PGRES_TUPLES_OK:
  150. /* it worked */
  151. S->is_prepared = 1;
  152. PQclear(S->result);
  153. break;
  154. default: {
  155. char *sqlstate = pdo_pgsql_sqlstate(S->result);
  156. /* 42P05 means that the prepared statement already existed. this can happen if you use
  157. * a connection pooling software line pgpool which doesn't close the db-connection once
  158. * php disconnects. if php dies (no chance to run RSHUTDOWN) during execution it has no
  159. * chance to DEALLOCATE the prepared statements it has created. so, if we hit a 42P05 we
  160. * deallocate it and retry ONCE (thies 2005.12.15)
  161. */
  162. if (sqlstate && !strcmp(sqlstate, "42P05")) {
  163. char buf[100]; /* stmt_name == "pdo_crsr_%08x" */
  164. PGresult *res;
  165. snprintf(buf, sizeof(buf), "DEALLOCATE %s", S->stmt_name);
  166. res = PQexec(H->server, buf);
  167. if (res) {
  168. PQclear(res);
  169. }
  170. goto stmt_retry;
  171. } else {
  172. pdo_pgsql_error_stmt(stmt, status, sqlstate);
  173. return 0;
  174. }
  175. }
  176. }
  177. }
  178. S->result = PQexecPrepared(H->server, S->stmt_name,
  179. stmt->bound_params ?
  180. zend_hash_num_elements(stmt->bound_params) :
  181. 0,
  182. (const char**)S->param_values,
  183. S->param_lengths,
  184. S->param_formats,
  185. 0);
  186. } else if (stmt->supports_placeholders == PDO_PLACEHOLDER_NAMED) {
  187. /* execute query with parameters */
  188. S->result = PQexecParams(H->server, S->query,
  189. stmt->bound_params ? zend_hash_num_elements(stmt->bound_params) : 0,
  190. S->param_types,
  191. (const char**)S->param_values,
  192. S->param_lengths,
  193. S->param_formats,
  194. 0);
  195. } else {
  196. /* execute plain query (with embedded parameters) */
  197. S->result = PQexec(H->server, stmt->active_query_string);
  198. }
  199. status = PQresultStatus(S->result);
  200. if (status != PGRES_COMMAND_OK && status != PGRES_TUPLES_OK) {
  201. pdo_pgsql_error_stmt(stmt, status, pdo_pgsql_sqlstate(S->result));
  202. return 0;
  203. }
  204. if (!stmt->executed && !stmt->column_count) {
  205. stmt->column_count = (int) PQnfields(S->result);
  206. S->cols = ecalloc(stmt->column_count, sizeof(pdo_pgsql_column));
  207. }
  208. if (status == PGRES_COMMAND_OK) {
  209. stmt->row_count = (long)atoi(PQcmdTuples(S->result));
  210. H->pgoid = PQoidValue(S->result);
  211. } else {
  212. stmt->row_count = (long)PQntuples(S->result);
  213. }
  214. return 1;
  215. }
  216. static int pgsql_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *param,
  217. enum pdo_param_event event_type TSRMLS_DC)
  218. {
  219. pdo_pgsql_stmt *S = (pdo_pgsql_stmt*)stmt->driver_data;
  220. if (stmt->supports_placeholders == PDO_PLACEHOLDER_NAMED && param->is_param) {
  221. switch (event_type) {
  222. case PDO_PARAM_EVT_FREE:
  223. if (param->driver_data) {
  224. efree(param->driver_data);
  225. }
  226. break;
  227. case PDO_PARAM_EVT_NORMALIZE:
  228. /* decode name from $1, $2 into 0, 1 etc. */
  229. if (param->name) {
  230. if (param->name[0] == '$') {
  231. param->paramno = atoi(param->name + 1);
  232. } else {
  233. /* resolve parameter name to rewritten name */
  234. char *nameptr;
  235. if (stmt->bound_param_map && SUCCESS == zend_hash_find(stmt->bound_param_map,
  236. param->name, param->namelen + 1, (void**)&nameptr)) {
  237. param->paramno = atoi(nameptr + 1) - 1;
  238. } else {
  239. pdo_raise_impl_error(stmt->dbh, stmt, "HY093", param->name TSRMLS_CC);
  240. return 0;
  241. }
  242. }
  243. }
  244. break;
  245. case PDO_PARAM_EVT_ALLOC:
  246. case PDO_PARAM_EVT_EXEC_POST:
  247. case PDO_PARAM_EVT_FETCH_PRE:
  248. case PDO_PARAM_EVT_FETCH_POST:
  249. /* work is handled by EVT_NORMALIZE */
  250. return 1;
  251. case PDO_PARAM_EVT_EXEC_PRE:
  252. if (!stmt->bound_param_map) {
  253. return 0;
  254. }
  255. if (!S->param_values) {
  256. S->param_values = ecalloc(
  257. zend_hash_num_elements(stmt->bound_param_map),
  258. sizeof(char*));
  259. S->param_lengths = ecalloc(
  260. zend_hash_num_elements(stmt->bound_param_map),
  261. sizeof(int));
  262. S->param_formats = ecalloc(
  263. zend_hash_num_elements(stmt->bound_param_map),
  264. sizeof(int));
  265. S->param_types = ecalloc(
  266. zend_hash_num_elements(stmt->bound_param_map),
  267. sizeof(Oid));
  268. }
  269. if (param->paramno >= 0) {
  270. if (param->paramno > zend_hash_num_elements(stmt->bound_param_map)) {
  271. pdo_pgsql_error_stmt(stmt, PGRES_FATAL_ERROR, "HY105");
  272. return 0;
  273. }
  274. if (PDO_PARAM_TYPE(param->param_type) == PDO_PARAM_LOB &&
  275. Z_TYPE_P(param->parameter) == IS_RESOURCE) {
  276. php_stream *stm;
  277. php_stream_from_zval_no_verify(stm, &param->parameter);
  278. if (stm) {
  279. if (php_stream_is(stm, &pdo_pgsql_lob_stream_ops)) {
  280. struct pdo_pgsql_lob_self *self = (struct pdo_pgsql_lob_self*)stm->abstract;
  281. pdo_pgsql_bound_param *P = param->driver_data;
  282. if (P == NULL) {
  283. P = ecalloc(1, sizeof(*P));
  284. param->driver_data = P;
  285. }
  286. P->oid = htonl(self->oid);
  287. S->param_values[param->paramno] = (char*)&P->oid;
  288. S->param_lengths[param->paramno] = sizeof(P->oid);
  289. S->param_formats[param->paramno] = 1;
  290. S->param_types[param->paramno] = OIDOID;
  291. return 1;
  292. } else {
  293. int len;
  294. SEPARATE_ZVAL_IF_NOT_REF(&param->parameter);
  295. Z_TYPE_P(param->parameter) = IS_STRING;
  296. if ((len = php_stream_copy_to_mem(stm, &Z_STRVAL_P(param->parameter), PHP_STREAM_COPY_ALL, 0)) > 0) {
  297. Z_STRLEN_P(param->parameter) = len;
  298. } else {
  299. ZVAL_EMPTY_STRING(param->parameter);
  300. }
  301. }
  302. } else {
  303. /* expected a stream resource */
  304. pdo_pgsql_error_stmt(stmt, PGRES_FATAL_ERROR, "HY105");
  305. return 0;
  306. }
  307. }
  308. if (PDO_PARAM_TYPE(param->param_type) == PDO_PARAM_NULL ||
  309. Z_TYPE_P(param->parameter) == IS_NULL) {
  310. S->param_values[param->paramno] = NULL;
  311. S->param_lengths[param->paramno] = 0;
  312. } else if (Z_TYPE_P(param->parameter) == IS_BOOL) {
  313. S->param_values[param->paramno] = Z_BVAL_P(param->parameter) ? "t" : "f";
  314. S->param_lengths[param->paramno] = 1;
  315. S->param_formats[param->paramno] = 0;
  316. } else {
  317. SEPARATE_ZVAL_IF_NOT_REF(&param->parameter);
  318. convert_to_string(param->parameter);
  319. S->param_values[param->paramno] = Z_STRVAL_P(param->parameter);
  320. S->param_lengths[param->paramno] = Z_STRLEN_P(param->parameter);
  321. S->param_formats[param->paramno] = 0;
  322. }
  323. if (PDO_PARAM_TYPE(param->param_type) == PDO_PARAM_LOB) {
  324. S->param_types[param->paramno] = 0;
  325. S->param_formats[param->paramno] = 1;
  326. } else {
  327. S->param_types[param->paramno] = 0;
  328. }
  329. }
  330. break;
  331. }
  332. } else if (param->is_param) {
  333. /* We need to manually convert to a pg native boolean value */
  334. if (PDO_PARAM_TYPE(param->param_type) == PDO_PARAM_BOOL &&
  335. ((param->param_type & PDO_PARAM_INPUT_OUTPUT) != PDO_PARAM_INPUT_OUTPUT)) {
  336. SEPARATE_ZVAL(&param->parameter);
  337. param->param_type = PDO_PARAM_STR;
  338. ZVAL_STRINGL(param->parameter, Z_BVAL_P(param->parameter) ? "t" : "f", 1, 1);
  339. }
  340. }
  341. return 1;
  342. }
  343. static int pgsql_stmt_fetch(pdo_stmt_t *stmt,
  344. enum pdo_fetch_orientation ori, long offset TSRMLS_DC)
  345. {
  346. pdo_pgsql_stmt *S = (pdo_pgsql_stmt*)stmt->driver_data;
  347. if (S->cursor_name) {
  348. char *ori_str = NULL;
  349. char *q = NULL;
  350. ExecStatusType status;
  351. switch (ori) {
  352. case PDO_FETCH_ORI_NEXT: spprintf(&ori_str, 0, "NEXT"); break;
  353. case PDO_FETCH_ORI_PRIOR: spprintf(&ori_str, 0, "BACKWARD"); break;
  354. case PDO_FETCH_ORI_FIRST: spprintf(&ori_str, 0, "FIRST"); break;
  355. case PDO_FETCH_ORI_LAST: spprintf(&ori_str, 0, "LAST"); break;
  356. case PDO_FETCH_ORI_ABS: spprintf(&ori_str, 0, "ABSOLUTE %ld", offset); break;
  357. case PDO_FETCH_ORI_REL: spprintf(&ori_str, 0, "RELATIVE %ld", offset); break;
  358. default:
  359. return 0;
  360. }
  361. spprintf(&q, 0, "FETCH %s FROM %s", ori_str, S->cursor_name);
  362. efree(ori_str);
  363. S->result = PQexec(S->H->server, q);
  364. efree(q);
  365. status = PQresultStatus(S->result);
  366. if (status != PGRES_COMMAND_OK && status != PGRES_TUPLES_OK) {
  367. pdo_pgsql_error_stmt(stmt, status, pdo_pgsql_sqlstate(S->result));
  368. return 0;
  369. }
  370. if (PQntuples(S->result)) {
  371. S->current_row = 1;
  372. return 1;
  373. } else {
  374. return 0;
  375. }
  376. } else {
  377. if (S->current_row < stmt->row_count) {
  378. S->current_row++;
  379. return 1;
  380. } else {
  381. return 0;
  382. }
  383. }
  384. }
  385. static int pgsql_stmt_describe(pdo_stmt_t *stmt, int colno TSRMLS_DC)
  386. {
  387. pdo_pgsql_stmt *S = (pdo_pgsql_stmt*)stmt->driver_data;
  388. struct pdo_column_data *cols = stmt->columns;
  389. struct pdo_bound_param_data *param;
  390. if (!S->result) {
  391. return 0;
  392. }
  393. cols[colno].name = estrdup(PQfname(S->result, colno));
  394. cols[colno].namelen = strlen(cols[colno].name);
  395. cols[colno].maxlen = PQfsize(S->result, colno);
  396. cols[colno].precision = PQfmod(S->result, colno);
  397. S->cols[colno].pgsql_type = PQftype(S->result, colno);
  398. switch(S->cols[colno].pgsql_type) {
  399. case BOOLOID:
  400. cols[colno].param_type = PDO_PARAM_BOOL;
  401. break;
  402. case OIDOID:
  403. /* did the user bind the column as a LOB ? */
  404. if (stmt->bound_columns && (
  405. SUCCESS == zend_hash_index_find(stmt->bound_columns,
  406. colno, (void**)&param) ||
  407. SUCCESS == zend_hash_find(stmt->bound_columns,
  408. cols[colno].name, cols[colno].namelen,
  409. (void**)&param))) {
  410. if (PDO_PARAM_TYPE(param->param_type) == PDO_PARAM_LOB) {
  411. cols[colno].param_type = PDO_PARAM_LOB;
  412. break;
  413. }
  414. }
  415. cols[colno].param_type = PDO_PARAM_INT;
  416. break;
  417. case INT2OID:
  418. case INT4OID:
  419. cols[colno].param_type = PDO_PARAM_INT;
  420. break;
  421. case INT8OID:
  422. if (sizeof(long)>=8) {
  423. cols[colno].param_type = PDO_PARAM_INT;
  424. } else {
  425. cols[colno].param_type = PDO_PARAM_STR;
  426. }
  427. break;
  428. case BYTEAOID:
  429. cols[colno].param_type = PDO_PARAM_LOB;
  430. break;
  431. default:
  432. cols[colno].param_type = PDO_PARAM_STR;
  433. }
  434. return 1;
  435. }
  436. static int pgsql_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, unsigned long *len, int *caller_frees TSRMLS_DC)
  437. {
  438. pdo_pgsql_stmt *S = (pdo_pgsql_stmt*)stmt->driver_data;
  439. struct pdo_column_data *cols = stmt->columns;
  440. size_t tmp_len;
  441. if (!S->result) {
  442. return 0;
  443. }
  444. /* We have already increased count by 1 in pgsql_stmt_fetch() */
  445. if (PQgetisnull(S->result, S->current_row - 1, colno)) { /* Check if we got NULL */
  446. *ptr = NULL;
  447. *len = 0;
  448. } else {
  449. *ptr = PQgetvalue(S->result, S->current_row - 1, colno);
  450. *len = PQgetlength(S->result, S->current_row - 1, colno);
  451. switch(cols[colno].param_type) {
  452. case PDO_PARAM_INT:
  453. S->cols[colno].intval = atol(*ptr);
  454. *ptr = (char *) &(S->cols[colno].intval);
  455. *len = sizeof(long);
  456. break;
  457. case PDO_PARAM_BOOL:
  458. S->cols[colno].boolval = **ptr == 't' ? 1: 0;
  459. *ptr = (char *) &(S->cols[colno].boolval);
  460. *len = sizeof(zend_bool);
  461. break;
  462. case PDO_PARAM_LOB:
  463. if (S->cols[colno].pgsql_type == OIDOID) {
  464. /* ooo, a real large object */
  465. char *end_ptr;
  466. Oid oid = (Oid)strtoul(*ptr, &end_ptr, 10);
  467. int loid = lo_open(S->H->server, oid, INV_READ);
  468. if (loid >= 0) {
  469. *ptr = (char*)pdo_pgsql_create_lob_stream(stmt->dbh, loid, oid TSRMLS_CC);
  470. *len = 0;
  471. return *ptr ? 1 : 0;
  472. }
  473. *ptr = NULL;
  474. *len = 0;
  475. return 0;
  476. } else {
  477. char *tmp_ptr = (char *)PQunescapeBytea((unsigned char *)*ptr, &tmp_len);
  478. if (!tmp_ptr) {
  479. /* PQunescapeBytea returned an error */
  480. *len = 0;
  481. return 0;
  482. }
  483. if (!tmp_len) {
  484. /* Empty string, return as empty stream */
  485. *ptr = (char *)php_stream_memory_open(TEMP_STREAM_READONLY, "", 0);
  486. PQfreemem(tmp_ptr);
  487. *len = 0;
  488. } else {
  489. *ptr = estrndup(tmp_ptr, tmp_len);
  490. PQfreemem(tmp_ptr);
  491. *len = tmp_len;
  492. *caller_frees = 1;
  493. }
  494. }
  495. break;
  496. case PDO_PARAM_NULL:
  497. case PDO_PARAM_STR:
  498. case PDO_PARAM_STMT:
  499. case PDO_PARAM_INPUT_OUTPUT:
  500. case PDO_PARAM_ZVAL:
  501. default:
  502. break;
  503. }
  504. }
  505. return 1;
  506. }
  507. static int pgsql_stmt_get_column_meta(pdo_stmt_t *stmt, long colno, zval *return_value TSRMLS_DC)
  508. {
  509. pdo_pgsql_stmt *S = (pdo_pgsql_stmt*)stmt->driver_data;
  510. PGresult *res;
  511. char *q=NULL;
  512. ExecStatusType status;
  513. if (!S->result) {
  514. return FAILURE;
  515. }
  516. if (colno >= stmt->column_count) {
  517. return FAILURE;
  518. }
  519. array_init(return_value);
  520. add_assoc_long(return_value, "pgsql:oid", S->cols[colno].pgsql_type);
  521. /* Fetch metadata from Postgres system catalogue */
  522. spprintf(&q, 0, "SELECT TYPNAME FROM PG_TYPE WHERE OID=%u", S->cols[colno].pgsql_type);
  523. res = PQexec(S->H->server, q);
  524. efree(q);
  525. status = PQresultStatus(res);
  526. if (status != PGRES_TUPLES_OK) {
  527. /* Failed to get system catalogue, but return success
  528. * with the data we have collected so far
  529. */
  530. goto done;
  531. }
  532. /* We want exactly one row returned */
  533. if (1 != PQntuples(res)) {
  534. goto done;
  535. }
  536. add_assoc_string(return_value, "native_type", PQgetvalue(res, 0, 0));
  537. done:
  538. PQclear(res);
  539. return 1;
  540. }
  541. static int pdo_pgsql_stmt_cursor_closer(pdo_stmt_t *stmt TSRMLS_DC)
  542. {
  543. return 1;
  544. }
  545. struct pdo_stmt_methods pgsql_stmt_methods = {
  546. pgsql_stmt_dtor,
  547. pgsql_stmt_execute,
  548. pgsql_stmt_fetch,
  549. pgsql_stmt_describe,
  550. pgsql_stmt_get_col,
  551. pgsql_stmt_param_hook,
  552. NULL, /* set_attr */
  553. NULL, /* get_attr */
  554. pgsql_stmt_get_column_meta,
  555. NULL, /* next_rowset */
  556. pdo_pgsql_stmt_cursor_closer
  557. };
  558. /*
  559. * Local variables:
  560. * tab-width: 4
  561. * c-basic-offset: 4
  562. * End:
  563. * vim600: noet sw=4 ts=4 fdm=marker
  564. * vim<600: noet sw=4 ts=4
  565. */