1 /* assuan-handler.c - dispatch commands
2 * Copyright (C) 2001, 2002, 2003, 2007 Free Software Foundation, Inc.
4 * This file is part of Assuan.
6 * Assuan is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU Lesser General Public License as
8 * published by the Free Software Foundation; either version 2.1 of
9 * the License, or (at your option) any later version.
11 * Assuan is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this program; if not, see <http://www.gnu.org/licenses/>.
26 #include "assuan-defs.h"
30 #define spacep(p) (*(p) == ' ' || *(p) == '\t')
31 #define digitp(a) ((a) >= '0' && (a) <= '9')
33 static int my_strcasecmp (const char *a
, const char *b
);
36 #define PROCESS_DONE(ctx, rc) \
37 ((ctx)->in_process_next ? assuan_process_done ((ctx), (rc)) : (rc))
40 dummy_handler (assuan_context_t ctx
, char *line
)
43 PROCESS_DONE (ctx
, set_error (ctx
, Server_Fault
, "no handler registered"));
48 std_handler_nop (assuan_context_t ctx
, char *line
)
50 return PROCESS_DONE (ctx
, 0); /* okay */
54 std_handler_cancel (assuan_context_t ctx
, char *line
)
56 if (ctx
->cancel_notify_fnc
)
57 ctx
->cancel_notify_fnc (ctx
);
58 return PROCESS_DONE (ctx
, set_error (ctx
, Not_Implemented
, NULL
));
62 std_handler_option (assuan_context_t ctx
, char *line
)
64 char *key
, *value
, *p
;
66 for (key
=line
; spacep (key
); key
++)
70 PROCESS_DONE (ctx
, set_error (ctx
, Syntax_Error
, "argument required"));
73 PROCESS_DONE (ctx
, set_error (ctx
, Syntax_Error
,
74 "no option name given"));
75 for (value
=key
; *value
&& !spacep (value
) && *value
!= '='; value
++)
80 *value
++ = 0; /* terminate key */
81 for (; spacep (value
); value
++)
85 *value
++ = 0; /* terminate key */
86 for (; spacep (value
); value
++)
90 PROCESS_DONE (ctx
, set_error (ctx
, Syntax_Error
,
91 "option argument expected"));
95 for (p
= value
+ strlen(value
) - 1; p
> value
&& spacep (p
); p
--)
98 *++p
= 0; /* strip trailing spaces */
102 if (*key
== '-' && key
[1] == '-' && key
[2])
103 key
+= 2; /* the double dashes are optional */
105 return PROCESS_DONE (ctx
,
106 set_error (ctx
, Syntax_Error
,
107 "option should not begin with one dash"));
109 if (ctx
->option_handler_fnc
)
110 return PROCESS_DONE (ctx
, ctx
->option_handler_fnc (ctx
, key
, value
));
111 return PROCESS_DONE (ctx
, 0);
115 std_handler_bye (assuan_context_t ctx
, char *line
)
117 if (ctx
->bye_notify_fnc
)
118 ctx
->bye_notify_fnc (ctx
);
119 assuan_close_input_fd (ctx
);
120 assuan_close_output_fd (ctx
);
121 return PROCESS_DONE (ctx
, _assuan_error (-1)); /* pretty simple :-) */
125 std_handler_auth (assuan_context_t ctx
, char *line
)
127 return PROCESS_DONE (ctx
, set_error (ctx
, Not_Implemented
, NULL
));
131 std_handler_reset (assuan_context_t ctx
, char *line
)
133 if (ctx
->reset_notify_fnc
)
134 ctx
->reset_notify_fnc (ctx
);
135 assuan_close_input_fd (ctx
);
136 assuan_close_output_fd (ctx
);
137 _assuan_uds_close_fds (ctx
);
138 return PROCESS_DONE (ctx
, 0);
142 std_handler_help (assuan_context_t ctx
, char *line
)
145 char buf
[ASSUAN_LINELENGTH
];
147 for (i
= 0; i
< ctx
->cmdtbl_used
; i
++)
149 snprintf (buf
, sizeof (buf
), "# %s", ctx
->cmdtbl
[i
].name
);
150 buf
[ASSUAN_LINELENGTH
- 1] = '\0';
151 assuan_write_line (ctx
, buf
);
154 return PROCESS_DONE (ctx
, 0);
159 std_handler_end (assuan_context_t ctx
, char *line
)
161 return PROCESS_DONE (ctx
, set_error (ctx
, Not_Implemented
, NULL
));
166 assuan_command_parse_fd (assuan_context_t ctx
, char *line
, assuan_fd_t
*rfd
)
170 if ((strncmp (line
, "FD", 2) && strncmp (line
, "fd", 2))
171 || (line
[2] != '=' && line
[2] != '\0' && !spacep(&line
[2])))
172 return set_error (ctx
, Syntax_Error
, "FD[=<n>] expected");
178 return set_error (ctx
, Syntax_Error
, "number required");
179 #ifdef HAVE_W32_SYSTEM
180 /* Fixme: For a W32/64bit system we will need to change the cast
181 and the conversion fucntion. */
182 *rfd
= (void*)strtoul (line
, &endp
, 10);
184 *rfd
= strtoul (line
, &endp
, 10);
186 /* Remove that argument so that a notify handler won't see it. */
187 memset (line
, ' ', endp
? (endp
-line
):strlen(line
));
189 if (*rfd
== ctx
->inbound
.fd
)
190 return set_error (ctx
, Parameter_Conflict
, "fd same as inbound fd");
191 if (*rfd
== ctx
->outbound
.fd
)
192 return set_error (ctx
, Parameter_Conflict
, "fd same as outbound fd");
196 /* Our peer has sent the file descriptor. */
197 return assuan_receivefd (ctx
, rfd
);
201 /* Format is INPUT FD=<n> */
203 std_handler_input (assuan_context_t ctx
, char *line
)
208 rc
= assuan_command_parse_fd (ctx
, line
, &fd
);
210 return PROCESS_DONE (ctx
, rc
);
212 if (ctx
->input_notify_fnc
)
213 ctx
->input_notify_fnc (ctx
, line
);
214 return PROCESS_DONE (ctx
, 0);
217 /* Format is OUTPUT FD=<n> */
219 std_handler_output (assuan_context_t ctx
, char *line
)
224 rc
= assuan_command_parse_fd (ctx
, line
, &fd
);
226 return PROCESS_DONE (ctx
, rc
);
228 if (ctx
->output_notify_fnc
)
229 ctx
->output_notify_fnc (ctx
, line
);
230 return PROCESS_DONE (ctx
, 0);
237 /* This is a table with the standard commands and handler for them.
238 The table is used to initialize a new context and associate strings
239 with default handlers */
242 int (*handler
)(assuan_context_t
, char *line
);
243 int always
; /* always initialize this command */
244 } std_cmd_table
[] = {
245 { "NOP", std_handler_nop
, 1 },
246 { "CANCEL", std_handler_cancel
, 1 },
247 { "OPTION", std_handler_option
, 1 },
248 { "BYE", std_handler_bye
, 1 },
249 { "AUTH", std_handler_auth
, 1 },
250 { "RESET", std_handler_reset
, 1 },
251 { "END", std_handler_end
, 1 },
252 { "HELP", std_handler_help
, 1 },
254 { "INPUT", std_handler_input
, 0 },
255 { "OUTPUT", std_handler_output
, 0 },
261 * assuan_register_command:
262 * @ctx: the server context
263 * @cmd_name: A string with the command name
264 * @handler: The handler function to be called or NULL to use a default
267 * Register a handler to be used for a given command. Note that
268 * several default handlers are already regsitered with a new context.
269 * This function however allows to override them.
271 * Return value: 0 on success or an error code
274 assuan_register_command (assuan_context_t ctx
,
275 const char *cmd_name
,
276 int (*handler
)(assuan_context_t
, char *))
281 if (cmd_name
&& !*cmd_name
)
285 return _assuan_error (ASSUAN_Invalid_Value
);
288 { /* find a default handler. */
289 for (i
=0; (s
=std_cmd_table
[i
].name
) && strcmp (cmd_name
, s
); i
++)
292 { /* Try again but case insensitive. */
293 for (i
=0; (s
=std_cmd_table
[i
].name
)
294 && my_strcasecmp (cmd_name
, s
); i
++)
298 handler
= std_cmd_table
[i
].handler
;
300 handler
= dummy_handler
; /* Last resort is the dummy handler. */
305 ctx
->cmdtbl_size
= 50;
306 ctx
->cmdtbl
= xtrycalloc ( ctx
->cmdtbl_size
, sizeof *ctx
->cmdtbl
);
308 return _assuan_error (ASSUAN_Out_Of_Core
);
309 ctx
->cmdtbl_used
= 0;
311 else if (ctx
->cmdtbl_used
>= ctx
->cmdtbl_size
)
315 x
= xtryrealloc ( ctx
->cmdtbl
, (ctx
->cmdtbl_size
+10) * sizeof *x
);
317 return _assuan_error (ASSUAN_Out_Of_Core
);
319 ctx
->cmdtbl_size
+= 50;
322 ctx
->cmdtbl
[ctx
->cmdtbl_used
].name
= cmd_name
;
323 ctx
->cmdtbl
[ctx
->cmdtbl_used
].handler
= handler
;
329 assuan_register_post_cmd_notify (assuan_context_t ctx
,
330 void (*fnc
)(assuan_context_t
, int))
333 return _assuan_error (ASSUAN_Invalid_Value
);
334 ctx
->post_cmd_notify_fnc
= fnc
;
339 assuan_register_pre_cmd_notify (assuan_context_t ctx
,
340 int (*fnc
)(assuan_context_t
, const char *))
343 return _assuan_error (ASSUAN_Invalid_Value
);
344 ctx
->pre_cmd_notify_fnc
= fnc
;
349 assuan_register_bye_notify (assuan_context_t ctx
,
350 void (*fnc
)(assuan_context_t
))
353 return _assuan_error (ASSUAN_Invalid_Value
);
354 ctx
->bye_notify_fnc
= fnc
;
359 assuan_register_reset_notify (assuan_context_t ctx
,
360 void (*fnc
)(assuan_context_t
))
363 return _assuan_error (ASSUAN_Invalid_Value
);
364 ctx
->reset_notify_fnc
= fnc
;
369 assuan_register_cancel_notify (assuan_context_t ctx
,
370 void (*fnc
)(assuan_context_t
))
373 return _assuan_error (ASSUAN_Invalid_Value
);
374 ctx
->cancel_notify_fnc
= fnc
;
379 assuan_register_option_handler (assuan_context_t ctx
,
380 int (*fnc
)(assuan_context_t
,
381 const char*, const char*))
384 return _assuan_error (ASSUAN_Invalid_Value
);
385 ctx
->option_handler_fnc
= fnc
;
390 assuan_register_input_notify (assuan_context_t ctx
,
391 void (*fnc
)(assuan_context_t
, const char *))
394 return _assuan_error (ASSUAN_Invalid_Value
);
395 ctx
->input_notify_fnc
= fnc
;
400 assuan_register_output_notify (assuan_context_t ctx
,
401 void (*fnc
)(assuan_context_t
, const char *))
404 return _assuan_error (ASSUAN_Invalid_Value
);
405 ctx
->output_notify_fnc
= fnc
;
410 /* Helper to register the standards commands */
412 _assuan_register_std_commands (assuan_context_t ctx
)
416 for (i
=0; std_cmd_table
[i
].name
; i
++)
418 if (std_cmd_table
[i
].always
)
420 rc
= assuan_register_command (ctx
, std_cmd_table
[i
].name
, NULL
);
430 /* Process the special data lines. The "D " has already been removed
431 from the line. As all handlers this function may modify the line. */
433 handle_data_line (assuan_context_t ctx
, char *line
, int linelen
)
435 return set_error (ctx
, Not_Implemented
, NULL
);
438 /* like ascii_strcasecmp but assume that B is already uppercase */
440 my_strcasecmp (const char *a
, const char *b
)
445 for (; *a
&& *b
; a
++, b
++)
447 if (((*a
>= 'a' && *a
<= 'z')? (*a
&~0x20):*a
) != *b
)
450 return *a
== *b
? 0 : (((*a
>= 'a' && *a
<= 'z')? (*a
&~0x20):*a
) - *b
);
454 /* Parse the line, break out the command, find it in the command
455 table, remove leading and white spaces from the arguments, call the
456 handler with the argument line and return the error. */
458 dispatch_command (assuan_context_t ctx
, char *line
, int linelen
)
465 /* Note that as this function is invoked by assuan_process_next as
466 well, we need to hide non-critical errors with PROCESS_DONE. */
468 if (*line
== 'D' && line
[1] == ' ') /* divert to special handler */
469 /* FIXME: Depending on the final implementation of
470 handle_data_line, this may be wrong here. For example, if a
471 user callback is invoked, and that callback is responsible for
472 calling assuan_process_done, then this is wrong. */
473 return PROCESS_DONE (ctx
, handle_data_line (ctx
, line
+2, linelen
-2));
475 for (p
=line
; *p
&& *p
!= ' ' && *p
!= '\t'; p
++)
479 (ctx
, set_error (ctx
, Syntax_Error
, "leading white-space"));
481 { /* Skip over leading WS after the keyword */
483 while ( *p
== ' ' || *p
== '\t')
488 for (i
=0; (s
=ctx
->cmdtbl
[i
].name
); i
++)
490 if (!strcmp (line
, s
))
494 { /* and try case insensitive */
495 for (i
=0; (s
=ctx
->cmdtbl
[i
].name
); i
++)
497 if (!my_strcasecmp (line
, s
))
502 return PROCESS_DONE (ctx
, set_error (ctx
, Unknown_Command
, NULL
));
506 if (ctx
->pre_cmd_notify_fnc
) {
507 rc
= ctx
->pre_cmd_notify_fnc(ctx
, ctx
->cmdtbl
[i
].name
);
510 return PROCESS_DONE (ctx
, rc
);
513 /* fprintf (stderr, "DBG-assuan: processing %s `%s'\n", s, line); */
514 return ctx
->cmdtbl
[i
].handler (ctx
, line
);
518 /* Call this to acknowledge the current command. */
520 assuan_process_done (assuan_context_t ctx
, int rc
)
522 if (!ctx
->in_command
)
523 return _assuan_error (ASSUAN_General_Error
);
527 /* Check for data write errors. */
528 if (ctx
->outbound
.data
.fp
)
530 /* Flush the data lines. */
531 fclose (ctx
->outbound
.data
.fp
);
532 ctx
->outbound
.data
.fp
= NULL
;
533 if (!rc
&& ctx
->outbound
.data
.error
)
534 rc
= ctx
->outbound
.data
.error
;
538 /* Flush any data send without using the data FP. */
539 assuan_send_data (ctx
, NULL
, 0);
540 if (!rc
&& ctx
->outbound
.data
.error
)
541 rc
= ctx
->outbound
.data
.error
;
544 /* Error handling. */
547 rc
= assuan_write_line (ctx
, ctx
->okay_line
? ctx
->okay_line
: "OK");
549 else if (err_is_eof (rc
))
550 { /* No error checking because the peer may have already disconnect. */
551 assuan_write_line (ctx
, "OK closing connection");
552 ctx
->finish_handler (ctx
);
559 sprintf (errline
, "ERR %d server fault (%.50s)",
560 _assuan_error (ASSUAN_Server_Fault
), assuan_strerror (rc
));
563 const char *text
= ctx
->err_no
== rc
? ctx
->err_str
:NULL
;
565 #if defined(HAVE_W32_SYSTEM)
566 unsigned int source
, code
;
570 source
= ((rc
>> 24) & 0xff);
571 code
= (rc
& 0x00ffffff);
573 && !_assuan_gpg_strerror_r (rc
, ebuf
, sizeof ebuf
)
574 && (esrc
=_assuan_gpg_strsource (rc
)))
576 /* Assume this is an libgpg-error. */
577 sprintf (errline
, "ERR %d %.50s <%.30s>%s%.100s",
579 text
? " - ":"", text
?text
:"");
582 #elif defined(__GNUC__) && defined(__ELF__)
583 /* If we have weak symbol support we try to use the error
584 strings from libgpg-error without creating a dependency.
585 They are used for debugging purposes only, so there is no
586 problem if they are not available. We need to make sure
587 that we are using ELF because only this guarantees that
588 weak symbol support is available in case GNU ld is not
589 used. It seems that old gcc versions don't implement the
590 weak attribute properly but it works with the weak
593 unsigned int source
, code
;
595 int gpg_strerror_r (unsigned int err
, char *buf
, size_t buflen
)
596 __attribute__ ((weak
));
597 const char *gpg_strsource (unsigned int err
)
598 __attribute__ ((weak
));
600 #pragma weak gpg_strerror_r
601 #pragma weak gpg_strsource
604 source
= ((rc
>> 24) & 0xff);
605 code
= (rc
& 0x00ffffff);
606 if (source
&& gpg_strsource
&& gpg_strerror_r
)
608 /* Assume this is an libgpg-error. */
611 gpg_strerror_r (rc
, ebuf
, sizeof ebuf
);
612 sprintf (errline
, "ERR %d %.50s <%.30s>%s%.100s",
616 text
? " - ":"", text
?text
:"");
619 #endif /* __GNUC__ && __ELF__ */
620 sprintf (errline
, "ERR %d %.50s%s%.100s",
621 rc
, assuan_strerror (rc
), text
? " - ":"", text
?text
:"");
623 rc
= assuan_write_line (ctx
, errline
);
626 if (ctx
->post_cmd_notify_fnc
)
627 ctx
->post_cmd_notify_fnc (ctx
, rc
);
629 ctx
->confidential
= 0;
632 xfree (ctx
->okay_line
);
633 ctx
->okay_line
= NULL
;
641 process_next (assuan_context_t ctx
)
645 /* What the next thing to do is depends on the current state.
646 However, we will always first read the next line. The client is
647 required to write full lines without blocking long after starting
649 rc
= _assuan_read_line (ctx
);
650 if (_assuan_error_is_eagain (rc
))
654 if (*ctx
->inbound
.line
== '#' || !ctx
->inbound
.linelen
)
655 /* Comment lines are ignored. */
658 /* Now we have a line that really means something. It could be one
659 of the following things: First, if we are not in a command
660 already, it is the next command to dispatch. Second, if we are
661 in a command, it can only be the response to an INQUIRE
664 if (!ctx
->in_command
)
668 ctx
->outbound
.data
.error
= 0;
669 ctx
->outbound
.data
.linelen
= 0;
670 /* Dispatch command and return reply. */
671 ctx
->in_process_next
= 1;
672 rc
= dispatch_command (ctx
, ctx
->inbound
.line
, ctx
->inbound
.linelen
);
673 ctx
->in_process_next
= 0;
675 else if (ctx
->in_inquire
)
677 /* FIXME: Pick up the continuation. */
678 rc
= _assuan_inquire_ext_cb (ctx
);
682 /* Should not happen. The client is sending data while we are
683 in a command and not waiting for an inquire. We log an error
685 _assuan_log_printf ("unexpected client data\n");
693 /* This function should be invoked when the assuan connected FD is
694 ready for reading. If the equivalent to EWOULDBLOCK is returned
695 (this should be done by the command handler), assuan_process_next
696 should be invoked the next time the connected FD is readable.
697 Eventually, the caller will finish by invoking
698 assuan_process_done. */
700 assuan_process_next (assuan_context_t ctx
)
706 rc
= process_next (ctx
);
708 while (!rc
&& assuan_pending_line (ctx
));
716 process_request (assuan_context_t ctx
)
721 return _assuan_error (ASSUAN_Nested_Commands
);
725 rc
= _assuan_read_line (ctx
);
727 while (_assuan_error_is_eagain (rc
));
730 if (*ctx
->inbound
.line
== '#' || !ctx
->inbound
.linelen
)
731 return 0; /* comment line - ignore */
734 ctx
->outbound
.data
.error
= 0;
735 ctx
->outbound
.data
.linelen
= 0;
736 /* dispatch command and return reply */
737 rc
= dispatch_command (ctx
, ctx
->inbound
.line
, ctx
->inbound
.linelen
);
739 return assuan_process_done (ctx
, rc
);
744 * @ctx: assuan context
746 * This function is used to handle the assuan protocol after a
747 * connection has been established using assuan_accept(). This is the
748 * main protocol handler.
750 * Return value: 0 on success or an error code if the assuan operation
751 * failed. Note, that no error is returned for operational errors.
754 assuan_process (assuan_context_t ctx
)
759 rc
= process_request (ctx
);
770 * assuan_get_active_fds:
771 * @ctx: Assuan context
772 * @what: 0 for read fds, 1 for write fds
773 * @fdarray: Caller supplied array to store the FDs
774 * @fdarraysize: size of that array
776 * Return all active filedescriptors for the given context. This
777 * function can be used to select on the fds and call
778 * assuan_process_next() if there is an active one. The first fd in
779 * the array is the one used for the command connection.
781 * Note, that write FDs are not yet supported.
783 * Return value: number of FDs active and put into @fdarray or -1 on
784 * error which is most likely a too small fdarray.
787 assuan_get_active_fds (assuan_context_t ctx
, int what
,
788 assuan_fd_t
*fdarray
, int fdarraysize
)
792 if (!ctx
|| fdarraysize
< 2 || what
< 0 || what
> 1)
797 if (ctx
->inbound
.fd
!= ASSUAN_INVALID_FD
)
798 fdarray
[n
++] = ctx
->inbound
.fd
;
802 if (ctx
->outbound
.fd
!= ASSUAN_INVALID_FD
)
803 fdarray
[n
++] = ctx
->outbound
.fd
;
804 if (ctx
->outbound
.data
.fp
)
805 #ifdef HAVE_W32_SYSTEM
806 fdarray
[n
++] = (void*)_get_osfhandle (fileno (ctx
->outbound
.data
.fp
));
808 fdarray
[n
++] = fileno (ctx
->outbound
.data
.fp
);
816 /* Two simple wrappers to make the expected function types match. */
819 fun1_cookie_write (void *cookie
, const char *buffer
, int orig_size
)
821 return _assuan_cookie_write_data (cookie
, buffer
, orig_size
);
823 #endif /*HAVE_FUNOPEN*/
824 #ifdef HAVE_FOPENCOOKIE
826 fun2_cookie_write (void *cookie
, const char *buffer
, size_t orig_size
)
828 return _assuan_cookie_write_data (cookie
, buffer
, orig_size
);
830 #endif /*HAVE_FOPENCOOKIE*/
832 /* Return a FP to be used for data output. The FILE pointer is valid
833 until the end of a handler. So a close is not needed. Assuan does
834 all the buffering needed to insert the status line as well as the
835 required line wappping and quoting for data lines.
837 We use GNU's custom streams here. There should be an alternative
838 implementaion for systems w/o a glibc, a simple implementation
839 could use a child process */
841 assuan_get_data_fp (assuan_context_t ctx
)
843 #if defined (HAVE_FOPENCOOKIE) || defined (HAVE_FUNOPEN)
844 if (ctx
->outbound
.data
.fp
)
845 return ctx
->outbound
.data
.fp
;
848 ctx
->outbound
.data
.fp
= funopen (ctx
, 0, fun1_cookie_write
,
849 0, _assuan_cookie_write_flush
);
851 ctx
->outbound
.data
.fp
= funopen (ctx
, 0, fun2_cookie_write
,
852 0, _assuan_cookie_write_flush
);
855 ctx
->outbound
.data
.error
= 0;
856 return ctx
->outbound
.data
.fp
;
864 /* Set the text used for the next OK reponse. This string is
865 automatically reset to NULL after the next command. */
867 assuan_set_okay_line (assuan_context_t ctx
, const char *line
)
870 return _assuan_error (ASSUAN_Invalid_Value
);
873 xfree (ctx
->okay_line
);
874 ctx
->okay_line
= NULL
;
878 /* FIXME: we need to use gcry_is_secure() to test whether
879 we should allocate the entire line in secure memory */
880 char *buf
= xtrymalloc (3+strlen(line
)+1);
882 return _assuan_error (ASSUAN_Out_Of_Core
);
884 strcpy (buf
+3, line
);
885 xfree (ctx
->okay_line
);
886 ctx
->okay_line
= buf
;
894 assuan_write_status (assuan_context_t ctx
,
895 const char *keyword
, const char *text
)
902 if ( !ctx
|| !keyword
)
903 return _assuan_error (ASSUAN_Invalid_Value
);
907 n
= 2 + strlen (keyword
) + 1 + strlen (text
) + 1;
908 if (n
< sizeof (buffer
))
910 strcpy (buffer
, "S ");
911 strcat (buffer
, keyword
);
914 strcat (buffer
, " ");
915 strcat (buffer
, text
);
917 ae
= assuan_write_line (ctx
, buffer
);
919 else if ( (helpbuf
= xtrymalloc (n
)) )
921 strcpy (helpbuf
, "S ");
922 strcat (helpbuf
, keyword
);
925 strcat (helpbuf
, " ");
926 strcat (helpbuf
, text
);
928 ae
= assuan_write_line (ctx
, helpbuf
);