Line data Source code
1 : /*
2 : * psql - the PostgreSQL interactive terminal
3 : *
4 : * Copyright (c) 2000-2024, PostgreSQL Global Development Group
5 : *
6 : * src/bin/psql/copy.c
7 : */
8 : #include "postgres_fe.h"
9 :
10 : #include <signal.h>
11 : #include <sys/stat.h>
12 : #ifndef WIN32
13 : #include <unistd.h> /* for isatty */
14 : #else
15 : #include <io.h> /* I think */
16 : #endif
17 :
18 : #include "common.h"
19 : #include "common/logging.h"
20 : #include "copy.h"
21 : #include "libpq-fe.h"
22 : #include "pqexpbuffer.h"
23 : #include "prompt.h"
24 : #include "settings.h"
25 : #include "stringutils.h"
26 :
27 : /*
28 : * parse_slash_copy
29 : * -- parses \copy command line
30 : *
31 : * The documented syntax is:
32 : * \copy tablename [(columnlist)] from|to filename [options]
33 : * \copy ( query stmt ) to filename [options]
34 : *
35 : * where 'filename' can be one of the following:
36 : * '<file path>' | PROGRAM '<command>' | stdin | stdout | pstdout | pstdout
37 : * and 'query' can be one of the following:
38 : * SELECT | UPDATE | INSERT | DELETE
39 : *
40 : * An undocumented fact is that you can still write BINARY before the
41 : * tablename; this is a hangover from the pre-7.3 syntax. The options
42 : * syntax varies across backend versions, but we avoid all that mess
43 : * by just transmitting the stuff after the filename literally.
44 : *
45 : * table name can be double-quoted and can have a schema part.
46 : * column names can be double-quoted.
47 : * filename can be single-quoted like SQL literals.
48 : * command must be single-quoted like SQL literals.
49 : *
50 : * returns a malloc'ed structure with the options, or NULL on parsing error
51 : */
52 :
53 : struct copy_options
54 : {
55 : char *before_tofrom; /* COPY string before TO/FROM */
56 : char *after_tofrom; /* COPY string after TO/FROM filename */
57 : char *file; /* NULL = stdin/stdout */
58 : bool program; /* is 'file' a program to popen? */
59 : bool psql_inout; /* true = use psql stdin/stdout */
60 : bool from; /* true = FROM, false = TO */
61 : };
62 :
63 :
64 : static void
65 162 : free_copy_options(struct copy_options *ptr)
66 : {
67 162 : if (!ptr)
68 0 : return;
69 162 : free(ptr->before_tofrom);
70 162 : free(ptr->after_tofrom);
71 162 : free(ptr->file);
72 162 : free(ptr);
73 : }
74 :
75 :
76 : /* concatenate "more" onto "var", freeing the original value of *var */
77 : static void
78 1008 : xstrcat(char **var, const char *more)
79 : {
80 : char *newvar;
81 :
82 1008 : newvar = psprintf("%s%s", *var, more);
83 1008 : free(*var);
84 1008 : *var = newvar;
85 1008 : }
86 :
87 :
88 : static struct copy_options *
89 162 : parse_slash_copy(const char *args)
90 : {
91 : struct copy_options *result;
92 : char *token;
93 162 : const char *whitespace = " \t\n\r";
94 162 : char nonstd_backslash = standard_strings() ? 0 : '\\';
95 :
96 162 : if (!args)
97 : {
98 0 : pg_log_error("\\copy: arguments required");
99 0 : return NULL;
100 : }
101 :
102 162 : result = pg_malloc0(sizeof(struct copy_options));
103 :
104 162 : result->before_tofrom = pg_strdup(""); /* initialize for appending */
105 :
106 162 : token = strtokx(args, whitespace, ".,()", "\"",
107 : 0, false, false, pset.encoding);
108 162 : if (!token)
109 0 : goto error;
110 :
111 : /* The following can be removed when we drop 7.3 syntax support */
112 162 : if (pg_strcasecmp(token, "binary") == 0)
113 : {
114 0 : xstrcat(&result->before_tofrom, token);
115 0 : token = strtokx(NULL, whitespace, ".,()", "\"",
116 : 0, false, false, pset.encoding);
117 0 : if (!token)
118 0 : goto error;
119 : }
120 :
121 : /* Handle COPY (query) case */
122 162 : if (token[0] == '(')
123 : {
124 24 : int parens = 1;
125 :
126 366 : while (parens > 0)
127 : {
128 342 : xstrcat(&result->before_tofrom, " ");
129 342 : xstrcat(&result->before_tofrom, token);
130 342 : token = strtokx(NULL, whitespace, "()", "\"'",
131 : nonstd_backslash, true, false, pset.encoding);
132 342 : if (!token)
133 0 : goto error;
134 342 : if (token[0] == '(')
135 18 : parens++;
136 324 : else if (token[0] == ')')
137 42 : parens--;
138 : }
139 : }
140 :
141 162 : xstrcat(&result->before_tofrom, " ");
142 162 : xstrcat(&result->before_tofrom, token);
143 162 : token = strtokx(NULL, whitespace, ".,()", "\"",
144 : 0, false, false, pset.encoding);
145 162 : if (!token)
146 0 : goto error;
147 :
148 : /*
149 : * strtokx() will not have returned a multi-character token starting with
150 : * '.', so we don't need strcmp() here. Likewise for '(', etc, below.
151 : */
152 162 : if (token[0] == '.')
153 : {
154 : /* handle schema . table */
155 0 : xstrcat(&result->before_tofrom, token);
156 0 : token = strtokx(NULL, whitespace, ".,()", "\"",
157 : 0, false, false, pset.encoding);
158 0 : if (!token)
159 0 : goto error;
160 0 : xstrcat(&result->before_tofrom, token);
161 0 : token = strtokx(NULL, whitespace, ".,()", "\"",
162 : 0, false, false, pset.encoding);
163 0 : if (!token)
164 0 : goto error;
165 : }
166 :
167 162 : if (token[0] == '(')
168 : {
169 : /* handle parenthesized column list */
170 : for (;;)
171 : {
172 0 : xstrcat(&result->before_tofrom, " ");
173 0 : xstrcat(&result->before_tofrom, token);
174 0 : token = strtokx(NULL, whitespace, "()", "\"",
175 : 0, false, false, pset.encoding);
176 0 : if (!token)
177 0 : goto error;
178 0 : if (token[0] == ')')
179 0 : break;
180 : }
181 0 : xstrcat(&result->before_tofrom, " ");
182 0 : xstrcat(&result->before_tofrom, token);
183 0 : token = strtokx(NULL, whitespace, ".,()", "\"",
184 : 0, false, false, pset.encoding);
185 0 : if (!token)
186 0 : goto error;
187 : }
188 :
189 162 : if (pg_strcasecmp(token, "from") == 0)
190 102 : result->from = true;
191 60 : else if (pg_strcasecmp(token, "to") == 0)
192 60 : result->from = false;
193 : else
194 0 : goto error;
195 :
196 : /* { 'filename' | PROGRAM 'command' | STDIN | STDOUT | PSTDIN | PSTDOUT } */
197 162 : token = strtokx(NULL, whitespace, ";", "'",
198 : 0, false, false, pset.encoding);
199 162 : if (!token)
200 0 : goto error;
201 :
202 162 : if (pg_strcasecmp(token, "program") == 0)
203 : {
204 : int toklen;
205 :
206 0 : token = strtokx(NULL, whitespace, ";", "'",
207 : 0, false, false, pset.encoding);
208 0 : if (!token)
209 0 : goto error;
210 :
211 : /*
212 : * The shell command must be quoted. This isn't fool-proof, but
213 : * catches most quoting errors.
214 : */
215 0 : toklen = strlen(token);
216 0 : if (token[0] != '\'' || toklen < 2 || token[toklen - 1] != '\'')
217 0 : goto error;
218 :
219 0 : strip_quotes(token, '\'', 0, pset.encoding);
220 :
221 0 : result->program = true;
222 0 : result->file = pg_strdup(token);
223 : }
224 314 : else if (pg_strcasecmp(token, "stdin") == 0 ||
225 152 : pg_strcasecmp(token, "stdout") == 0)
226 : {
227 70 : result->file = NULL;
228 : }
229 184 : else if (pg_strcasecmp(token, "pstdin") == 0 ||
230 92 : pg_strcasecmp(token, "pstdout") == 0)
231 : {
232 0 : result->psql_inout = true;
233 0 : result->file = NULL;
234 : }
235 : else
236 : {
237 : /* filename can be optionally quoted */
238 92 : strip_quotes(token, '\'', 0, pset.encoding);
239 92 : result->file = pg_strdup(token);
240 92 : expand_tilde(&result->file);
241 : }
242 :
243 : /* Collect the rest of the line (COPY options) */
244 162 : token = strtokx(NULL, "", NULL, NULL,
245 : 0, false, false, pset.encoding);
246 162 : if (token)
247 50 : result->after_tofrom = pg_strdup(token);
248 :
249 162 : return result;
250 :
251 0 : error:
252 0 : if (token)
253 0 : pg_log_error("\\copy: parse error at \"%s\"", token);
254 : else
255 0 : pg_log_error("\\copy: parse error at end of line");
256 0 : free_copy_options(result);
257 :
258 0 : return NULL;
259 : }
260 :
261 :
262 : /*
263 : * Execute a \copy command (frontend copy). We have to open a file (or execute
264 : * a command), then submit a COPY query to the backend and either feed it data
265 : * from the file or route its response into the file.
266 : */
267 : bool
268 162 : do_copy(const char *args)
269 : {
270 : PQExpBufferData query;
271 : FILE *copystream;
272 : struct copy_options *options;
273 : bool success;
274 :
275 : /* parse options */
276 162 : options = parse_slash_copy(args);
277 :
278 162 : if (!options)
279 0 : return false;
280 :
281 : /* prepare to read or write the target file */
282 162 : if (options->file && !options->program)
283 92 : canonicalize_path(options->file);
284 :
285 162 : if (options->from)
286 : {
287 102 : if (options->file)
288 : {
289 92 : if (options->program)
290 : {
291 0 : fflush(NULL);
292 0 : errno = 0;
293 0 : copystream = popen(options->file, PG_BINARY_R);
294 : }
295 : else
296 92 : copystream = fopen(options->file, PG_BINARY_R);
297 : }
298 10 : else if (!options->psql_inout)
299 10 : copystream = pset.cur_cmd_source;
300 : else
301 0 : copystream = stdin;
302 : }
303 : else
304 : {
305 60 : if (options->file)
306 : {
307 0 : if (options->program)
308 : {
309 0 : fflush(NULL);
310 0 : disable_sigpipe_trap();
311 0 : errno = 0;
312 0 : copystream = popen(options->file, PG_BINARY_W);
313 : }
314 : else
315 0 : copystream = fopen(options->file, PG_BINARY_W);
316 : }
317 60 : else if (!options->psql_inout)
318 60 : copystream = pset.queryFout;
319 : else
320 0 : copystream = stdout;
321 : }
322 :
323 162 : if (!copystream)
324 : {
325 10 : if (options->program)
326 0 : pg_log_error("could not execute command \"%s\": %m",
327 : options->file);
328 : else
329 10 : pg_log_error("%s: %m",
330 : options->file);
331 10 : free_copy_options(options);
332 10 : return false;
333 : }
334 :
335 152 : if (!options->program)
336 : {
337 : struct stat st;
338 : int result;
339 :
340 : /* make sure the specified file is not a directory */
341 152 : if ((result = fstat(fileno(copystream), &st)) < 0)
342 0 : pg_log_error("could not stat file \"%s\": %m",
343 : options->file);
344 :
345 152 : if (result == 0 && S_ISDIR(st.st_mode))
346 0 : pg_log_error("%s: cannot copy from/to a directory",
347 : options->file);
348 :
349 152 : if (result < 0 || S_ISDIR(st.st_mode))
350 : {
351 0 : fclose(copystream);
352 0 : free_copy_options(options);
353 0 : return false;
354 : }
355 : }
356 :
357 : /* build the command we will send to the backend */
358 152 : initPQExpBuffer(&query);
359 152 : printfPQExpBuffer(&query, "COPY ");
360 152 : appendPQExpBufferStr(&query, options->before_tofrom);
361 152 : if (options->from)
362 92 : appendPQExpBufferStr(&query, " FROM STDIN ");
363 : else
364 60 : appendPQExpBufferStr(&query, " TO STDOUT ");
365 152 : if (options->after_tofrom)
366 44 : appendPQExpBufferStr(&query, options->after_tofrom);
367 :
368 : /* run it like a user command, but with copystream as data source/sink */
369 152 : pset.copyStream = copystream;
370 152 : success = SendQuery(query.data);
371 152 : pset.copyStream = NULL;
372 152 : termPQExpBuffer(&query);
373 :
374 152 : if (options->file != NULL)
375 : {
376 82 : if (options->program)
377 : {
378 0 : int pclose_rc = pclose(copystream);
379 :
380 0 : if (pclose_rc != 0)
381 : {
382 0 : if (pclose_rc < 0)
383 0 : pg_log_error("could not close pipe to external command: %m");
384 : else
385 : {
386 0 : char *reason = wait_result_to_str(pclose_rc);
387 :
388 0 : pg_log_error("%s: %s", options->file,
389 : reason ? reason : "");
390 0 : free(reason);
391 : }
392 0 : success = false;
393 : }
394 0 : SetShellResultVariables(pclose_rc);
395 0 : restore_sigpipe_trap();
396 : }
397 : else
398 : {
399 82 : if (fclose(copystream) != 0)
400 : {
401 0 : pg_log_error("%s: %m", options->file);
402 0 : success = false;
403 : }
404 : }
405 : }
406 152 : free_copy_options(options);
407 152 : return success;
408 : }
409 :
410 :
411 : /*
412 : * Functions for handling COPY IN/OUT data transfer.
413 : *
414 : * If you want to use COPY TO STDOUT/FROM STDIN in your application,
415 : * this is the code to steal ;)
416 : */
417 :
418 : /*
419 : * handleCopyOut
420 : * receives data as a result of a COPY ... TO STDOUT command
421 : *
422 : * conn should be a database connection that you just issued COPY TO on
423 : * and got back a PGRES_COPY_OUT result.
424 : *
425 : * copystream is the file stream for the data to go to.
426 : * copystream can be NULL to eat the data without writing it anywhere.
427 : *
428 : * The final status for the COPY is returned into *res (but note
429 : * we already reported the error, if it's not a success result).
430 : *
431 : * result is true if successful, false if not.
432 : */
433 : bool
434 522 : handleCopyOut(PGconn *conn, FILE *copystream, PGresult **res)
435 : {
436 522 : bool OK = true;
437 : char *buf;
438 : int ret;
439 :
440 : for (;;)
441 : {
442 2316 : ret = PQgetCopyData(conn, &buf, 0);
443 :
444 2316 : if (ret < 0)
445 522 : break; /* done or server/connection error */
446 :
447 1794 : if (buf)
448 : {
449 1794 : if (OK && copystream && fwrite(buf, 1, ret, copystream) != ret)
450 : {
451 0 : pg_log_error("could not write COPY data: %m");
452 : /* complain only once, keep reading data from server */
453 0 : OK = false;
454 : }
455 1794 : PQfreemem(buf);
456 : }
457 : }
458 :
459 522 : if (OK && copystream && fflush(copystream))
460 : {
461 0 : pg_log_error("could not write COPY data: %m");
462 0 : OK = false;
463 : }
464 :
465 522 : if (ret == -2)
466 : {
467 0 : pg_log_error("COPY data transfer failed: %s", PQerrorMessage(conn));
468 0 : OK = false;
469 : }
470 :
471 : /*
472 : * Check command status and return to normal libpq state.
473 : *
474 : * If for some reason libpq is still reporting PGRES_COPY_OUT state, we
475 : * would like to forcibly exit that state, since our caller would be
476 : * unable to distinguish that situation from reaching the next COPY in a
477 : * command string that happened to contain two consecutive COPY TO STDOUT
478 : * commands. However, libpq provides no API for doing that, and in
479 : * principle it's a libpq bug anyway if PQgetCopyData() returns -1 or -2
480 : * but hasn't exited COPY_OUT state internally. So we ignore the
481 : * possibility here.
482 : */
483 522 : *res = PQgetResult(conn);
484 522 : if (PQresultStatus(*res) != PGRES_COMMAND_OK)
485 : {
486 2 : pg_log_info("%s", PQerrorMessage(conn));
487 2 : OK = false;
488 : }
489 :
490 522 : return OK;
491 : }
492 :
493 : /*
494 : * handleCopyIn
495 : * sends data to complete a COPY ... FROM STDIN command
496 : *
497 : * conn should be a database connection that you just issued COPY FROM on
498 : * and got back a PGRES_COPY_IN result.
499 : * copystream is the file stream to read the data from.
500 : * isbinary can be set from PQbinaryTuples().
501 : * The final status for the COPY is returned into *res (but note
502 : * we already reported the error, if it's not a success result).
503 : *
504 : * result is true if successful, false if not.
505 : */
506 :
507 : /* read chunk size for COPY IN - size is not critical */
508 : #define COPYBUFSIZ 8192
509 :
510 : bool
511 890 : handleCopyIn(PGconn *conn, FILE *copystream, bool isbinary, PGresult **res)
512 : {
513 : bool OK;
514 : char buf[COPYBUFSIZ];
515 : bool showprompt;
516 :
517 : /*
518 : * Establish longjmp destination for exiting from wait-for-input. (This is
519 : * only effective while sigint_interrupt_enabled is TRUE.)
520 : */
521 890 : if (sigsetjmp(sigint_interrupt_jmp, 1) != 0)
522 : {
523 : /* got here with longjmp */
524 :
525 : /* Terminate data transfer */
526 0 : PQputCopyEnd(conn,
527 0 : (PQprotocolVersion(conn) < 3) ? NULL :
528 0 : _("canceled by user"));
529 :
530 0 : OK = false;
531 0 : goto copyin_cleanup;
532 : }
533 :
534 : /* Prompt if interactive input */
535 890 : if (isatty(fileno(copystream)))
536 : {
537 0 : showprompt = true;
538 0 : if (!pset.quiet)
539 0 : puts(_("Enter data to be copied followed by a newline.\n"
540 : "End with a backslash and a period on a line by itself, or an EOF signal."));
541 : }
542 : else
543 890 : showprompt = false;
544 :
545 890 : OK = true;
546 :
547 890 : if (isbinary)
548 : {
549 : /* interactive input probably silly, but give one prompt anyway */
550 0 : if (showprompt)
551 : {
552 0 : const char *prompt = get_prompt(PROMPT_COPY, NULL);
553 :
554 0 : fputs(prompt, stdout);
555 0 : fflush(stdout);
556 : }
557 :
558 : for (;;)
559 0 : {
560 : int buflen;
561 :
562 : /* enable longjmp while waiting for input */
563 0 : sigint_interrupt_enabled = true;
564 :
565 0 : buflen = fread(buf, 1, COPYBUFSIZ, copystream);
566 :
567 0 : sigint_interrupt_enabled = false;
568 :
569 0 : if (buflen <= 0)
570 0 : break;
571 :
572 0 : if (PQputCopyData(conn, buf, buflen) <= 0)
573 : {
574 0 : OK = false;
575 0 : break;
576 : }
577 : }
578 : }
579 : else
580 : {
581 890 : bool copydone = false;
582 : int buflen;
583 890 : bool at_line_begin = true;
584 :
585 : /*
586 : * In text mode, we have to read the input one line at a time, so that
587 : * we can stop reading at the EOF marker (\.). We mustn't read beyond
588 : * the EOF marker, because if the data was inlined in a SQL script, we
589 : * would eat up the commands after the EOF marker.
590 : */
591 890 : buflen = 0;
592 73306 : while (!copydone)
593 : {
594 : char *fgresult;
595 :
596 72416 : if (at_line_begin && showprompt)
597 : {
598 0 : const char *prompt = get_prompt(PROMPT_COPY, NULL);
599 :
600 0 : fputs(prompt, stdout);
601 0 : fflush(stdout);
602 : }
603 :
604 : /* enable longjmp while waiting for input */
605 72416 : sigint_interrupt_enabled = true;
606 :
607 72416 : fgresult = fgets(&buf[buflen], COPYBUFSIZ - buflen, copystream);
608 :
609 72416 : sigint_interrupt_enabled = false;
610 :
611 72416 : if (!fgresult)
612 80 : copydone = true;
613 : else
614 : {
615 : int linelen;
616 :
617 72336 : linelen = strlen(fgresult);
618 72336 : buflen += linelen;
619 :
620 : /* current line is done? */
621 72336 : if (buf[buflen - 1] == '\n')
622 : {
623 : /*
624 : * When at the beginning of the line and the data is
625 : * inlined, check for EOF marker. If the marker is found,
626 : * we must stop at this point. If not, the \. line can be
627 : * sent to the server, and we let it decide whether it's
628 : * an EOF or not depending on the format: in TEXT mode, \.
629 : * will be interpreted as an EOF, in CSV, it will not.
630 : */
631 72212 : if (at_line_begin && copystream == pset.cur_cmd_source)
632 : {
633 3434 : if ((linelen == 3 && memcmp(fgresult, "\\.\n", 3) == 0) ||
634 72 : (linelen == 4 && memcmp(fgresult, "\\.\r\n", 4) == 0))
635 : {
636 810 : copydone = true;
637 :
638 : /*
639 : * Remove the EOF marker from the data sent. In
640 : * CSV mode, the EOF marker must be removed,
641 : * otherwise it would be interpreted by the server
642 : * as valid data.
643 : */
644 810 : *fgresult = '\0';
645 810 : buflen -= linelen;
646 : }
647 : }
648 :
649 72212 : if (copystream == pset.cur_cmd_source)
650 : {
651 3452 : pset.lineno++;
652 3452 : pset.stmt_lineno++;
653 : }
654 72212 : at_line_begin = true;
655 : }
656 : else
657 124 : at_line_begin = false;
658 : }
659 :
660 : /*
661 : * If the buffer is full, or we've reached the EOF, flush it.
662 : *
663 : * Make sure there's always space for four more bytes in the
664 : * buffer, plus a NUL terminator. That way, an EOF marker is
665 : * never split across two fgets() calls, which simplifies the
666 : * logic.
667 : */
668 72416 : if (buflen >= COPYBUFSIZ - 5 || (copydone && buflen > 0))
669 : {
670 1032 : if (PQputCopyData(conn, buf, buflen) <= 0)
671 : {
672 0 : OK = false;
673 0 : break;
674 : }
675 :
676 1032 : buflen = 0;
677 : }
678 : }
679 : }
680 :
681 : /* Check for read error */
682 890 : if (ferror(copystream))
683 0 : OK = false;
684 :
685 : /*
686 : * Terminate data transfer. We can't send an error message if we're using
687 : * protocol version 2. (libpq no longer supports protocol version 2, but
688 : * keep the version checks just in case you're using a pre-v14 libpq.so at
689 : * runtime)
690 : */
691 890 : if (PQputCopyEnd(conn,
692 0 : (OK || PQprotocolVersion(conn) < 3) ? NULL :
693 0 : _("aborted because of read failure")) <= 0)
694 0 : OK = false;
695 :
696 890 : copyin_cleanup:
697 :
698 : /*
699 : * Clear the EOF flag on the stream, in case copying ended due to an EOF
700 : * signal. This allows an interactive TTY session to perform another COPY
701 : * FROM STDIN later. (In non-STDIN cases, we're about to close the file
702 : * anyway, so it doesn't matter.) Although we don't ever test the flag
703 : * with feof(), some fread() implementations won't read more data if it's
704 : * set. This also clears the error flag, but we already checked that.
705 : */
706 890 : clearerr(copystream);
707 :
708 : /*
709 : * Check command status and return to normal libpq state.
710 : *
711 : * We do not want to return with the status still PGRES_COPY_IN: our
712 : * caller would be unable to distinguish that situation from reaching the
713 : * next COPY in a command string that happened to contain two consecutive
714 : * COPY FROM STDIN commands. We keep trying PQputCopyEnd() in the hope
715 : * it'll work eventually. (What's actually likely to happen is that in
716 : * attempting to flush the data, libpq will eventually realize that the
717 : * connection is lost. But that's fine; it will get us out of COPY_IN
718 : * state, which is what we need.)
719 : */
720 890 : while (*res = PQgetResult(conn), PQresultStatus(*res) == PGRES_COPY_IN)
721 : {
722 0 : OK = false;
723 0 : PQclear(*res);
724 : /* We can't send an error message if we're using protocol version 2 */
725 0 : PQputCopyEnd(conn,
726 0 : (PQprotocolVersion(conn) < 3) ? NULL :
727 0 : _("trying to exit copy mode"));
728 : }
729 890 : if (PQresultStatus(*res) != PGRES_COMMAND_OK)
730 : {
731 198 : pg_log_info("%s", PQerrorMessage(conn));
732 198 : OK = false;
733 : }
734 :
735 890 : return OK;
736 : }
|