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