4 * This module contains decoding routines for converting
5 * quoted-printable data into pure 8-bit data, in MIME
8 * By Henrik Storner <storner@image.dk>
10 * Configuration file support for fetchmail 4.3.8 by
11 * Frank Damgaard <frda@post3.tele.dk>
13 * Modified by Hiroyuki Yamamoto <hiro-y@kcn.ne.jp>
30 # include "prefs_common.h"
43 #define xalloca(ptr, t, n) if (!(ptr = (t) alloca(n))) \
44 {fprintf(stderr, "alloca failed"); exit(1);}
46 static unsigned char unhex(unsigned char c
)
48 if ((c
>= '0') && (c
<= '9'))
50 else if ((c
>= 'A') && (c
<= 'F'))
51 return (c
- 'A' + 10);
52 else if ((c
>= 'a') && (c
<= 'f'))
53 return (c
- 'a' + 10);
58 static int qp_char(unsigned char c1
, unsigned char c2
, unsigned char *c_out
)
63 if ((c1
> 15) || (c2
> 15))
73 * Routines to decode MIME QP-encoded headers, as per RFC 2047.
76 /* States of the decoding state machine */
77 #define S_COPY_PLAIN 0 /* Just copy, but watch for the QP flag */
78 #define S_SKIP_MIMEINIT 1 /* Get the encoding, and skip header */
79 #define S_COPY_MIME 2 /* Decode a sequence of coded characters */
81 static const char MIMEHDR_INIT
[] = "=?"; /* Start of coded sequence */
82 static const char MIMEHDR_END
[] = "?="; /* End of coded sequence */
85 void UnMimeHeader(unsigned char *hdr
)
87 /* Decode a buffer containing data encoded according to RFC
88 * 2047. This only handles content-transfer-encoding; conversion
89 * between character sets is not implemented. In other words: We
90 * assume the charsets used can be displayed by your mail program
94 /* Note: Decoding is done "in-situ", i.e. without using an
95 * additional buffer for temp. storage. This is possible, since the
96 * decoded string will always be shorter than the encoded string,
97 * due to the en- coding scheme.
100 int state
= S_COPY_PLAIN
;
101 unsigned char *p_in
, *p_out
, *p
;
102 unsigned char enc
= '\0'; /* initialization pacifies -Wall */
105 /* Speed up in case this is not a MIME-encoded header */
106 p
= strstr(hdr
, MIMEHDR_INIT
);
108 return; /* No MIME header */
110 /* Loop through the buffer.
111 * p_in : Next char to be processed.
112 * p_out: Where to put the next processed char
113 * enc : Encoding used (usually, 'q' = quoted-printable)
115 for (p_out
= p_in
= hdr
; (*p_in
); ) {
118 p
= strstr(p_in
, MIMEHDR_INIT
);
121 * No more coded data in buffer,
122 * just move remainder into place.
124 i
= strlen(p_in
); /* How much left */
125 memmove(p_out
, p_in
, i
);
126 p_in
+= i
; p_out
+= i
;
129 /* MIME header init found at location p */
131 /* There are some uncoded chars at the beginning. */
133 memmove(p_out
, p_in
, i
);
137 state
= S_SKIP_MIMEINIT
;
141 case S_SKIP_MIMEINIT
:
142 /* Mime type definition: "charset?encoding?" */
143 p
= strchr(p_in
, '?');
145 /* p_in .. (p-1) holds the charset */
147 /* *(p+1) is the transfer encoding, *(p+2) must be a '?' */
149 enc
= tolower(*(p
+1));
154 state
= S_COPY_PLAIN
;
157 state
= S_COPY_PLAIN
; /* Invalid data */
161 p
= strstr(p_in
, MIMEHDR_END
); /* Find end of coded data */
162 if (p
== NULL
) p
= p_in
+ strlen(p_in
);
163 for (; (p_in
< p
); ) {
164 /* Decode all encoded data */
167 /* Decode one char qp-coded at (p_in+1) and (p_in+2) */
168 if (qp_char(*(p_in
+1), *(p_in
+2), p_out
) == 0)
171 /* Invalid QP data - pass through unchanged. */
176 else if (*p_in
== '_') {
178 * RFC 2047: '_' inside encoded word represents 0x20.
179 * NOT a space - always the value 0x20.
191 else if (enc
== 'b') {
192 /* Decode base64 encoded data */
196 delimsave
= *p
; *p
= '\r';
197 decoded_count
= from64tobits(p_out
, p_in
);
199 if (decoded_count
> 0)
200 p_out
+= decoded_count
;
211 p_in
+= 2; /* Skip the MIMEHDR_END delimiter */
214 * We've completed decoding one encoded sequence. But another
215 * may follow immediately, in which case whitespace before the
216 * new MIMEHDR_INIT delimiter must be discarded.
217 * See if that is the case
219 p
= strstr(p_in
, MIMEHDR_INIT
);
220 state
= S_COPY_PLAIN
;
223 * There is more MIME data later on. Is there
224 * whitespace only before the delimiter?
229 for (q
=p_in
; (wsp_only
&& (q
< p
)); q
++)
230 wsp_only
= isspace(*q
);
234 * Whitespace-only before the MIME delimiter. OK,
235 * just advance p_in to past the new MIMEHDR_INIT,
236 * and prepare to process the new MIME charset/encoding
239 p_in
= p
+ strlen(MIMEHDR_INIT
);
240 state
= S_SKIP_MIMEINIT
;
249 #else /* HAVE_LIBJCONV */
250 void UnMimeHeaderConv(unsigned char *hdr
, unsigned char *conv_r
, int conv_len
)
252 int state
= S_COPY_PLAIN
;
253 unsigned char *p_in
, *p_out
, *p
;
254 unsigned char enc
= '\0'; /* initialization pacifies -Wall */
256 unsigned char *p_mimestart
= NULL
;
257 iconv_t cd
= (iconv_t
)-1;
259 if (conv_r
&& conv_len
< 0) {
264 conv_len
--; /* reserve for terminating NULL character */
266 /* Speed up in case this is not a MIME-encoded header */
267 p
= strstr(hdr
, MIMEHDR_INIT
);
270 const char *const *codesets
;
271 int n_codesets
, actual_codeset
, r
;
275 if (prefs_common
.force_charset
) {
276 codesets
= (const char **)&prefs_common
.force_charset
;
280 codesets
= jconv_info_get_pref_codesets(&n_codesets
);
282 r
= jconv_alloc_conv(hdr
, strlen(hdr
), &newstr
, &newlen
,
283 codesets
, n_codesets
, &actual_codeset
,
284 jconv_info_get_current_codeset());
286 if (newlen
> conv_len
) newlen
= conv_len
;
287 strncpy(conv_r
, newstr
, newlen
);
288 conv_r
[newlen
] = '\0';
292 strncpy(conv_r
, hdr
, conv_len
);
293 conv_r
[conv_len
] = '\0';
299 /* Loop through the buffer.
300 * p_in : Next char to be processed.
301 * p_out: Where to put the next processed char
302 * enc : Encoding used (usually, 'q' = quoted-printable)
304 for (p_out
= p_in
= hdr
; (*p_in
); ) {
307 p
= strstr(p_in
, MIMEHDR_INIT
);
310 * No more coded data in buffer,
311 * just move remainder into place.
313 i
= strlen(p_in
); /* How much left */
317 len
= conv_len
> i
? i
: conv_len
;
318 memcpy(conv_r
, p_in
, len
);
322 memmove(p_out
, p_in
, i
);
323 p_in
+= i
; p_out
+= i
;
326 /* MIME header init found at location p */
328 /* There are some uncoded chars at the beginning. */
333 len
= conv_len
> i
? i
: conv_len
;
334 memcpy(conv_r
, p_in
, len
);
338 memmove(p_out
, p_in
, i
);
342 state
= S_SKIP_MIMEINIT
;
346 case S_SKIP_MIMEINIT
:
347 /* Mime type definition: "charset?encoding?" */
348 p
= strchr(p_in
, '?');
350 /* p_in .. (p-1) holds the charset */
353 charset
= malloc(p
- p_in
+ 1);
354 memcpy(charset
, p_in
, p
- p_in
);
355 charset
[p
- p_in
] = '\0';
356 if (cd
!= (iconv_t
)-1) iconv_close(cd
);
357 cd
= iconv_open(jconv_info_get_current_codeset(), charset
);
360 /* *(p+1) is the transfer encoding, *(p+2) must be a '?' */
362 enc
= tolower(*(p
+1));
367 state
= S_COPY_PLAIN
;
370 state
= S_COPY_PLAIN
; /* Invalid data */
375 p
= strstr(p_in
, MIMEHDR_END
); /* Find end of coded data */
376 if (p
== NULL
) p
= p_in
+ strlen(p_in
);
377 for (; (p_in
< p
); ) {
378 /* Decode all encoded data */
381 /* Decode one char qp-coded at (p_in+1) and (p_in+2) */
382 if (qp_char(*(p_in
+1), *(p_in
+2), p_out
) == 0)
385 /* Invalid QP data - pass through unchanged. */
390 else if (*p_in
== '_') {
392 * RFC 2047: '_' inside encoded word represents 0x20.
393 * NOT a space - always the value 0x20.
405 else if (enc
== 'b') {
406 /* Decode base64 encoded data */
410 delimsave
= *p
; *p
= '\r';
411 decoded_count
= from64tobits(p_out
, p_in
);
413 if (decoded_count
> 0)
414 p_out
+= decoded_count
;
424 if (conv_r
&& cd
!= (iconv_t
)-1) {
427 inleft
= p_out
- p_mimestart
;
428 iconv(cd
, (char **)&p_mimestart
, &inleft
, (char **)&conv_r
, &conv_len
);
431 p_in
+= 2; /* Skip the MIMEHDR_END delimiter */
434 * We've completed decoding one encoded sequence. But another
435 * may follow immediately, in which case whitespace before the
436 * new MIMEHDR_INIT delimiter must be discarded.
437 * See if that is the case
439 p
= strstr(p_in
, MIMEHDR_INIT
);
440 state
= S_COPY_PLAIN
;
443 * There is more MIME data later on. Is there
444 * whitespace only before the delimiter?
449 for (q
=p_in
; (wsp_only
&& (q
< p
)); q
++)
450 wsp_only
= isspace(*q
);
454 * Whitespace-only before the MIME delimiter. OK,
455 * just advance p_in to past the new MIMEHDR_INIT,
456 * and prepare to process the new MIME charset/encoding
459 p_in
= p
+ strlen(MIMEHDR_INIT
);
460 state
= S_SKIP_MIMEINIT
;
470 if (cd
!= (iconv_t
)-1) iconv_close(cd
);
473 #endif /* !HAVE_LIBJCONV */
477 * Routines for decoding body-parts of a message.
479 * Since the "fetch" part of fetchmail gets a message body
480 * one line at a time, we need to maintain some state variables
481 * across multiple invokations of the UnMimeBodyline() routine.
482 * The driver routine should call MimeBodyType() when all
483 * headers have been received, and then UnMimeBodyline() for
484 * every line in the message body.
487 #define S_BODY_DATA 0
491 * Flag indicating if we are currently processing
492 * the headers or the body of a (multipart) message.
494 static int BodyState
= S_BODY_DATA
;
497 * Flag indicating if we are in the process of decoding
498 * a quoted-printable body part.
500 static int CurrEncodingIsQP
= 0;
501 static int CurrTypeNeedsDecode
= 0;
504 * Delimiter for multipart messages. RFC 2046 states that this must
505 * NEVER be longer than 70 characters. Add 3 for the two hyphens
506 * at the beginning, and a terminating null.
508 #define MAX_DELIM_LEN 70
509 static unsigned char MultipartDelimiter
[MAX_DELIM_LEN
+3];
512 /* This string replaces the "Content-Transfer-Encoding: quoted-printable"
513 * string in all headers, including those in body-parts. The replacement
514 * must be no longer than the original string.
516 static const char ENC8BIT
[] = "Content-Transfer-Encoding: 8bit";
517 static void SetEncoding8bit(unsigned char *XferEncOfs
)
521 if (XferEncOfs
!= NULL
) {
522 memcpy(XferEncOfs
, ENC8BIT
, strlen(ENC8BIT
));
524 /* If anything left, in this header, replace with whitespace */
525 for (p
=XferEncOfs
+strlen(ENC8BIT
); (*p
>= ' '); p
++) *p
=' ';
529 static char *GetBoundary(char *CntType
)
534 /* Find the "boundary" delimiter. It must be preceded with a ';'
535 * and optionally some whitespace.
539 p2
= strchr(p1
, ';');
541 for (p2
++; isspace(*p2
); p2
++);
544 } while ((p1
) && (strncasecmp(p1
, "boundary", 8) != 0));
547 /* No boundary delimiter */
550 /* Skip "boundary", whitespace and '='; check that we do have a '=' */
551 for (p1
+=8, flag
=0; (isspace(*p1
) || (*p1
== '=')); p1
++)
552 flag
|= (*p1
== '=');
556 /* Find end of boundary delimiter string */
558 /* The delimiter is inside quotes */
560 p2
= strchr(p1
, '\"');
562 return NULL
; /* No closing '"' !?! */
565 /* There might be more text after the "boundary" string. */
566 p2
= strchr(p1
, ';'); /* Safe - delimiter with ';' must be in quotes */
569 /* Zero-terminate the boundary string */
573 return (p1
&& strlen(p1
)) ? p1
: NULL
;
577 int CheckContentType(char *CntType
)
580 * Static array of Content-Type's for which we will do
581 * quoted-printable decoding, if requested.
582 * It is probably wise to do this only on known text-only types;
583 * be really careful if you change this.
586 static char *DecodedTypes
[] = {
587 "text/", /* Will match ALL content-type's starting with 'text/' */
595 /* If no Content-Type header, it isn't MIME - don't touch it */
596 if (CntType
== NULL
) return 0;
598 /* Skip whitespace, if any */
599 for (; isspace(*p
); p
++) ;
603 (strncasecmp(p
, DecodedTypes
[i
], strlen(DecodedTypes
[i
]))));
606 return (DecodedTypes
[i
] != NULL
);
611 * This routine does three things:
612 * 1) It determines - based on the message headers - whether the
613 * message body is a MIME message that may hold 8 bit data.
614 * - A message that has a "quoted-printable" or "8bit" transfer
615 * encoding is assumed to contain 8-bit data (when decoded).
616 * - A multipart message is assumed to contain 8-bit data
617 * when decoded (there might be quoted-printable body-parts).
618 * - All other messages are assumed NOT to include 8-bit data.
619 * 2) It determines the delimiter-string used in multi-part message
621 * 3) It sets the initial values of the CurrEncodingIsQP,
622 * CurrTypeNeedsDecode, and BodyState variables, from the header
625 * The return value is a bitmask.
627 int MimeBodyType(unsigned char *hdrs
, int WantDecode
)
629 unsigned char *NxtHdr
= hdrs
;
630 unsigned char *XferEnc
, *XferEncOfs
, *CntType
, *MimeVer
, *p
;
631 int HdrsFound
= 0; /* We only look for three headers */
632 int BodyType
; /* Return value */
634 /* Setup for a standard (no MIME, no QP, 7-bit US-ASCII) message */
635 MultipartDelimiter
[0] = '\0';
636 CurrEncodingIsQP
= CurrTypeNeedsDecode
= 0;
637 BodyState
= S_BODY_DATA
;
640 /* Just in case ... */
644 XferEnc
= XferEncOfs
= CntType
= MimeVer
= NULL
;
647 if (strncasecmp("Content-Transfer-Encoding:", NxtHdr
, 26) == 0) {
651 xalloca(XferEnc
, char *, strlen(p
) + 1);
656 else if (strncasecmp("Content-Type:", NxtHdr
, 13) == 0) {
658 * This one is difficult. We cannot use the standard
659 * nxtaddr() routine, since the boundary-delimiter is
660 * (probably) enclosed in quotes - and thus appears
661 * as an rfc822 comment, and nxtaddr() "eats" up any
662 * spaces in the delimiter. So, we have to do this
666 /* Skip the "Content-Type:" part and whitespace after it */
667 for (NxtHdr
+= 13; ((*NxtHdr
== ' ') || (*NxtHdr
== '\t')); NxtHdr
++);
670 * Get the full value of the Content-Type header;
671 * it might span multiple lines. So search for
672 * a newline char, but ignore those that have a
673 * have a TAB or space just after the NL (continued
678 p
=strchr((p
+1),'\n');
679 } while ( (p
!= NULL
) && ((*(p
+1) == '\t') || (*(p
+1) == ' ')) );
680 if (p
== NULL
) p
= NxtHdr
+ strlen(NxtHdr
);
682 xalloca(CntType
, char *, p
-NxtHdr
+2);
683 strncpy(CntType
, NxtHdr
, (p
-NxtHdr
));
684 *(CntType
+(p
-NxtHdr
)) = '\0';
687 else if (strncasecmp("MIME-Version:", NxtHdr
, 13) == 0) {
690 xalloca(MimeVer
, char *, strlen(p
) + 1);
696 NxtHdr
= (strchr(NxtHdr
, '\n'));
697 if (NxtHdr
!= NULL
) NxtHdr
++;
698 } while ((NxtHdr
!= NULL
) && (*NxtHdr
) && (HdrsFound
!= 3));
701 /* Done looking through the headers, now check what they say */
702 if ((MimeVer
!= NULL
) && (strcmp(MimeVer
, "1.0") == 0)) {
704 CurrTypeNeedsDecode
= CheckContentType(CntType
);
706 /* Check Content-Type to see if this is a multipart message */
707 if ( (CntType
!= NULL
) &&
708 ((strncasecmp(CntType
, "multipart/mixed", 16) == 0) ||
709 (strncasecmp(CntType
, "message/", 8) == 0)) ) {
711 char *p1
= GetBoundary(CntType
);
714 /* The actual delimiter is "--" followed by
715 the boundary string */
716 strcpy(MultipartDelimiter
, "--");
717 strncat(MultipartDelimiter
, p1
, MAX_DELIM_LEN
);
718 BodyType
= (MSG_IS_8BIT
| MSG_NEEDS_DECODE
);
723 * Check Content-Transfer-Encoding, but
724 * ONLY for non-multipart messages (BodyType == 0).
726 if ((XferEnc
!= NULL
) && (BodyType
== 0)) {
727 if (strcasecmp(XferEnc
, "quoted-printable") == 0) {
728 CurrEncodingIsQP
= 1;
729 BodyType
= (MSG_IS_8BIT
| MSG_NEEDS_DECODE
);
730 if (WantDecode
&& CurrTypeNeedsDecode
) {
731 SetEncoding8bit(XferEncOfs
);
734 else if (strcasecmp(XferEnc
, "7bit") == 0) {
735 CurrEncodingIsQP
= 0;
736 BodyType
= (MSG_IS_7BIT
);
738 else if (strcasecmp(XferEnc
, "8bit") == 0) {
739 CurrEncodingIsQP
= 0;
740 BodyType
= (MSG_IS_8BIT
);
751 * Decode one line of data containing QP data.
752 * Return flag set if this line ends with a soft line-break.
753 * 'bufp' is modified to point to the end of the output buffer.
755 int DoOneQPLine(unsigned char **bufp
, flag delimited
, flag issoftline
)
757 unsigned char *buf
= *bufp
;
758 unsigned char *p_in
, *p_out
, *p
;
763 * Special case: line consists of a single =2E and messages are
764 * dot-terminated. Line has to be dot-stuffed after decoding.
766 if (delimited
&& !issoftline
&& buf
[0]=='=' && !strncmp(*bufp
, "=2E\n", 4))
774 if (delimited
&& issoftline
&& (strncmp(buf
, "..", 2) == 0))
777 for (p_out
= buf
; (*p_in
); ) {
778 p
= strchr(p_in
, '=');
780 /* No more QP data, just move remainder into place */
782 memmove(p_out
, p_in
, n
);
783 p_in
+= n
; p_out
+= n
;
787 /* There are some uncoded chars at the beginning. */
789 memmove(p_out
, p_in
, n
);
794 case '\0': case '\r': case '\n':
795 /* Soft line break, skip '=' */
797 if (*p_in
== '\r') p_in
++;
798 if (*p_in
== '\n') p_in
++;
803 /* There is a QP encoded byte */
804 if (qp_char(*(p
+1), *(p
+2), p_out
) == 0) {
808 /* Invalid QP data - pass through unchanged. */
824 /* This is called once per line in the message body. We need to scan
825 * all lines in the message body for the multipart delimiter string,
826 * and handle any body-part headers in such messages (these can toggle
827 * qp-decoding on and off).
829 * Note: Messages that are NOT multipart-messages go through this
830 * routine quickly, since BodyState will always be S_BODY_DATA,
831 * and MultipartDelimiter is NULL.
833 * Return flag set if this line ends with a soft line-break.
834 * 'bufp' is modified to point to the end of the output buffer.
838 int UnMimeBodyline(unsigned char **bufp
, flag delimited
, flag softline
)
840 unsigned char *buf
= *bufp
;
845 UnMimeHeader(buf
); /* Headers in body-parts can be encoded, too! */
846 if ((*buf
== '\0') || (*buf
== '\n') || (strcmp(buf
, "\r\n") == 0)) {
847 BodyState
= S_BODY_DATA
;
849 else if (strncasecmp("Content-Transfer-Encoding:", buf
, 26) == 0) {
852 XferEnc
= nxtaddr(buf
);
853 if ((XferEnc
!= NULL
) && (strcasecmp(XferEnc
, "quoted-printable") == 0)) {
854 CurrEncodingIsQP
= 1;
857 * Hmm ... we cannot be really sure that CurrTypeNeedsDecode
858 * has been set - we may not have seen the Content-Type header
859 * yet. But *usually* the Content-Type header comes first, so
860 * this will work. And there is really no way of doing it
861 * "right" as long as we stick with the line-by-line processing.
863 if (CurrTypeNeedsDecode
)
864 SetEncoding8bit(buf
);
867 else if (strncasecmp("Content-Type:", buf
, 13) == 0) {
868 CurrTypeNeedsDecode
= CheckContentType(nxtaddr(buf
));
871 *bufp
= (buf
+ strlen(buf
));
875 if ((*MultipartDelimiter
) &&
876 (strncmp(buf
, MultipartDelimiter
, strlen(MultipartDelimiter
)) == 0)) {
877 BodyState
= S_BODY_HDR
;
878 CurrEncodingIsQP
= CurrTypeNeedsDecode
= 0;
881 if (CurrEncodingIsQP
&& CurrTypeNeedsDecode
)
882 ret
= DoOneQPLine(bufp
, delimited
, softline
);
884 *bufp
= (buf
+ strlen(buf
));
897 char *program_name
= "unmime";
900 #define BUFSIZE_INCREMENT 4096
903 #define DBG_FWRITE(B,L,BS,FD) fwrite(B, L, BS, FD)
905 #define DBG_FWRITE(B,L,BS,FD)
908 int main(int argc
, char *argv
[])
910 unsigned int BufSize
;
911 unsigned char *buffer
, *buf_p
;
912 int nl_count
, i
, bodytype
;
916 FILE *fd_orig
, *fd_conv
;
920 sprintf(fnam
, "/tmp/i_unmime.%x", pid
);
921 fd_orig
= fopen(fnam
, "w");
922 sprintf(fnam
, "/tmp/o_unmime.%x", pid
);
923 fd_conv
= fopen(fnam
, "w");
926 BufSize
= BUFSIZE_INCREMENT
; /* Initial size of buffer */
927 buf_p
= buffer
= (unsigned char *) xmalloc(BufSize
);
931 i
= fread(buf_p
, 1, 1, stdin
);
946 if ((buf_p
- buffer
) == BufSize
) {
947 /* Buffer is full! Get more room. */
948 buffer
= xrealloc(buffer
, BufSize
+BUFSIZE_INCREMENT
);
949 buf_p
= buffer
+ BufSize
;
950 BufSize
+= BUFSIZE_INCREMENT
;
952 } while ((i
> 0) && (nl_count
< 2));
955 DBG_FWRITE(buffer
, strlen(buffer
), 1, fd_orig
);
957 UnMimeHeader(buffer
);
958 bodytype
= MimeBodyType(buffer
, 1);
961 fwrite(buffer
, i
, 1, stdout
);
962 DBG_FWRITE(buffer
, i
, 1, fd_conv
);
965 buf_p
= (buffer
- 1);
968 i
= fread(buf_p
, 1, 1, stdin
);
969 } while ((i
== 1) && (*buf_p
!= '\n'));
972 DBG_FWRITE(buf
, (buf_p
- buffer
), 1, fd_orig
);
974 if (buf_p
> buffer
) {
975 if (bodytype
& MSG_NEEDS_DECODE
) {
977 UnMimeBodyline(&buf_p
, 0);
979 fwrite(buffer
, (buf_p
- buffer
), 1, stdout
);
980 DBG_FWRITE(buffer
, (buf_p
- buffer
), 1, fd_conv
);
982 } while (buf_p
> buffer
);