/* $Id: decode.c,v 1.113 2015/12/09 03:39:11 onoe Exp $ */

/*-
 * Copyright (c) 1998-2001 Atsushi Onoe
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <utime.h>
#include <stddef.h>

#ifdef USE_ZLIB
#include <zlib.h>
#endif /* USE_ZLIB */

#include "cue.h"
#include "decode.h"
#include "b64.h"
#include "utf8.h"
#include "gbk.h"
#include "zip.h"

static int save_base64_decode(FILE *fp, ccbuf_t *buf);

const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const char hex[] = "0123456789abcdef0123456789ABCDEF";
const char uue[] = "`!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_";
const char hqx[] = "!\"#$%&'()*+,-012345689@ABCDEFGHIJKLMNPQRSTUVXYZ[`abcdefhijklmpqr";

char rb64[256], rhex[256], ruue[256], rhqx[256];

static struct charset {
	const char *cs_name;
	u_char	cs_enc;
	u_char	cs_code[3];
	const char *cs_alias[3];
	u_char	cs_len;
} charset_tbl[] = {
/* XXX The first 8 charsets MUST match the definition of FDB_CS_XXX in cue.h */
	{ "iso-2022-jp",    'B', { CS_KANJI, CS_KANA, CS_HOJO } }, /* default */
	{ "iso-2022-jp",    'B', { CS_KANJI, CS_KANA, CS_HOJO } },
	{ "shift_jis",	    'B', { CS_KANJI, CS_KANA, CS_HOJO },
	    { "sjis", "shift-jis" } },					/*XXX*/
	{ "utf-8",	    'B', { CS_KANJI, CS_KANA, CS_HOJO },
	    { "utf8" } },						/*XXX*/
	{ "us-ascii",       'Q', },
	{ "iso-2022-jp-2",  'B', },
	{ "cp943c",	    'Q', { CS_KANJI, CS_KANA, CS_HOJO } },	/*XXX*/
	{ "gb2312",	    'B', { CS_KANJI, CS_KANA, CS_HOJO },
	    { "gbk" } },						/*XXX*/

/* additional charsets */
	{ "euc-jp",	    'B', { CS_KANJI, CS_KANA, CS_HOJO },
	    { "eucjp" } },
	{ "iso-8859-1",	    'Q', { CS_96SET|CS_CODE('A'), CS_96SET|CS_CODE('A'),
				   CS_96SET|CS_CODE('A') } },
	{ "iso-8859-2",	    'Q', { CS_96SET|CS_CODE('B'), CS_96SET|CS_CODE('B'),
				   CS_96SET|CS_CODE('B') } },
	{ "iso-8859-3",	    'Q', { CS_96SET|CS_CODE('C'), CS_96SET|CS_CODE('C'),
				   CS_96SET|CS_CODE('C') } },
	{ "iso-8859-4",	    'Q', { CS_96SET|CS_CODE('D'), CS_96SET|CS_CODE('D'),
				   CS_96SET|CS_CODE('D') } },
	{ "iso-8859-5",	    'Q', { CS_96SET|CS_CODE('L'), CS_96SET|CS_CODE('L'),
				   CS_96SET|CS_CODE('L') } },
	{ "iso-8859-6",	    'Q', { CS_96SET|CS_CODE('G'), CS_96SET|CS_CODE('G'),
				   CS_96SET|CS_CODE('G') } },
	{ "iso-8859-7",	    'Q', { CS_96SET|CS_CODE('F'), CS_96SET|CS_CODE('F'),
				   CS_96SET|CS_CODE('F') } },
	{ "iso-8859-8",	    'Q', { CS_96SET|CS_CODE('H'), CS_96SET|CS_CODE('H'),
				   CS_96SET|CS_CODE('H') } },
	{ "iso-8859-9",	    'Q', { CS_96SET|CS_CODE('M'), CS_96SET|CS_CODE('M'),
				   CS_96SET|CS_CODE('M') } },
	{ "iso-8859-10",    'Q', { CS_96SET|CS_CODE('V'), CS_96SET|CS_CODE('V'),
				   CS_96SET|CS_CODE('V') } },
	{ "iso-8859-15",    'Q', { CS_96SET|CS_CODE('b'), CS_96SET|CS_CODE('b'),
				   CS_96SET|CS_CODE('b') } },
	{ "ks_c_5601-1987", 'B', { CS_DWIDTH|CS_CODE('C') },
	    { "euc-kr" } },
	{ "windows-1252",   'Q', { CS_96SET|CS_CODE('A'), CS_96SET|CS_CODE('A'),
				   CS_96SET|CS_CODE('A') } },	/* XXX */
	{ NULL, }
};

void
decode_init(void)
{
	int i;

	memset(rb64, -1, sizeof(rb64));
	for (i = 0; i < sizeof(b64); i++)
		rb64[((u_char *)b64)[i]] = i;
	memset(rhex, -1, sizeof(rhex));
	for (i = 0; i < sizeof(hex); i++)
		rhex[((u_char *)hex)[i]] = i & 0x0f;
	memset(ruue, -1, sizeof(ruue));
	for (i = 0; i < sizeof(uue); i++)
		ruue[((u_char *)uue)[i]] = i;
	ruue[' '] = 0;	/* XXX */
	memset(rhqx, -1, sizeof(rhqx));
	for (i = 0; i < sizeof(hqx); i++)
		rhqx[((u_char *)hqx)[i]] = i;

	for (i = 0; charset_tbl[i].cs_name != NULL; i++)
		charset_tbl[i].cs_len = strlen(charset_tbl[i].cs_name);
}

static void
charset_dinit(struct decode *dp, int id)
{
	int i;

	if (id < 0 && id >= sizeof(charset_tbl)/sizeof(charset_tbl[0]))
		id = 0;
	dp->cscode[0] = CS_ASCII;
	for (i = 1; i < 4; i++) {
		dp->cscode[i] = charset_tbl[id].cs_code[i - 1];
		if (dp->cscode[i] == 0)
			dp->cscode[i] = charset_tbl[0].cs_code[i - 1];
	}
	/* special handling */
	if (id == FDB_CS_SJIS || id == FDB_CS_SJIS2)
		dp->sjis = 1;
	else if (id == FDB_CS_UTF8)
		dp->utf8 = 1;
	else if (id == FDB_CS_GBK)
		dp->gbk = 1;
}

static void
DINIT(struct decode *dp, int flags)
{
	char *p;

	if (dp->bufsiz != CHARBLOCK) {
		p = realloc(dp->buf, CHARBLOCK);
		if (!p)
			abort();
		dp->buf = p;
		dp->bufsiz = CHARBLOCK;
	}
	dp->cnt = 0;
	dp->flsbuf = NULL;
	dp->pend = 0;
	dp->err = 0;
	dp->encoded = 0;
	dp->noconv = 0;
	dp->notext = 0;
	dp->sjis = 0;
	dp->utf8 = 0;
	dp->gbk = 0;
	dp->bclen = 0;
	dp->shift = 0;
	dp->html = 0;
	dp->skip = 0;
	dp->skiplen = 0;
	charset_dinit(dp, flags & FDB_CHARSET);
	if (flags & FDB_NOCONV)
		dp->noconv = 1;
	if (flags & FDB_NOTEXT)
		dp->notext = 1;
}

static void
OUTERR(struct decode *dp, u_char c)
{

	if (!dp->skip) {
		if (c < ' ' && c != '\n' && c != '\t') {
			OUTC(dp, '^'); OUTC(dp, c + '@');
			dp->encoded = 1;
		} else if (!(c & 0x80)) {
			OUTC(dp, c);
		} else {
			OUTC(dp, '\\'); OUTC(dp, 'x');
			OUTC(dp, hex[c >> 4]); OUTC(dp, hex[c & 0xf]);
			dp->encoded = 1;
		}
	}
	dp->err = 1;
}

static void
OUTPEND(struct decode *dp)
{
	int i;
	u_char c;

	if (!dp->skip) {
		for (i = 0; i < 4; i++) {
			c = dp->pend >> 24;
			dp->pend <<= 8;
			if (c != 0)
				OUTERR(dp, c);
		}
	}
	dp->pend = 0;
}

static int
skip_match(struct decode *dp, const char *str, int firstflag)
{
	int i;
	int len = strlen(str);
	int off = dp->skiplen;

	if (off < len || len > sizeof(dp->skipbuf))
		return 0;
	if (firstflag && off != len)
		return 0;
	off = (off - len) % sizeof(dp->skipbuf);
	for (i = 0; i < len; i++) {
		if (dp->skipbuf[(off + i) % sizeof(dp->skipbuf)] != str[i])
			return 0;
	}
	return 1;
}

static int
out_ampchar(struct decode *dp)
{
	int v;
	char *s = NULL;

	if (dp->skiplen > sizeof(dp->skipbuf) - 1)
		return 0;
	dp->skipbuf[dp->skiplen] = '\0';

	if (dp->skipbuf[0] == '#') {
		if (dp->skipbuf[1] == 'x')
			v = strtoul(dp->skipbuf + 2, NULL, 16);
		else
			v = strtoul(dp->skipbuf + 1, NULL, 10);
		if (v < 32 || v >= 127)
			return v;
		OUTC(dp, v);
		return 0;
	}
	if (skip_match(dp, "quot", 1))		v = '"';
	else if (skip_match(dp, "amp", 1))	v = '&';
	else if (skip_match(dp, "lt", 1))	v = '<';
	else if (skip_match(dp, "gt", 1))	v = '>';
	else if (skip_match(dp, "nbsp", 1))	v = ' ';
	else if (skip_match(dp, "copy", 1))	s = "(C)";
	else if (skip_match(dp, "reg", 1))	s = "(R)";
	else					return 1;
	if (s) {
		for (; *s; s++)
			OUTC(dp, *s);
	} else
		OUTC(dp, v);
	return 0;
}

static void
DOUTC(struct decode *dp, char c0)
{
	u_char c = (u_char)c0;
	u_char s;
	u_long v;
	int G = 0;
	int i;

	if (dp->noconv) {
		OUTC(dp, c);
		return;
	}
#if 0	/* try to decode and show */
	if (dp->notext) {
		OUTERR(dp, c);
		return;
	}
#endif
	if (dp->pend == '\r') {
		if (c != '\n') {
			OUTC(dp, '\n');
		}
		dp->pend = 0;
	}

	if (dp->html && dp->pend == 0 && dp->cscode[G] == CS_ASCII) {
		if (dp->skip == ';') {
			if (c == ';') {
				dp->skip = 0;
				if (out_ampchar(dp)) {
					OUTC(dp, '&');
					for (i = 0; i < dp->skiplen; i++)
						OUTC(dp, dp->skipbuf[i]);
					OUTC(dp, ';');
				}
			} else
				SKIP_PUT(dp, c);
			return;
		}
		if (dp->skip) {
			if (c == 0x1b) {
				dp->pend = c;
				return;
			}
			if (dp->skip == c) {
				dp->skip = 0;
				return;
			}
			SKIP_PUT(dp, c);
			switch (dp->skip) {
			case '>':
				if (skip_match(dp, "!--", 1))
					dp->skip = SKIP_COMMENT;
				else if (skip_match(dp, "style", 1))
					dp->skip = SKIP_STYLE;
				else if (skip_match(dp, "script", 1))
					dp->skip = SKIP_SCRIPT;
				break;
			case SKIP_COMMENT:
				if (skip_match(dp, "-->", 0))
					dp->skip = 0;
				break;
			case SKIP_STYLE:
				if (skip_match(dp, "/style>", 0))
					dp->skip = 0;
				break;
			case SKIP_SCRIPT:
				if (skip_match(dp, "/script>", 0))
					dp->skip = 0;
				break;
			}
			return;
		}
		if (c == '<') {
			dp->skip = '>';
			dp->encoded = 1;
			dp->skiplen = 0;
			return;
		}
		if (c == '&') {
			dp->skip = ';';
			dp->encoded = 1;
			dp->skiplen = 0;
			return;
		}
	}

	if (dp->sjis == 1 && (c & 0x80)) {		/* SJIS 1st */
		if (dp->pend)
			OUTPEND(dp);
		if (c >= 0xa1 && c <= 0xdf) {		/* KANA */
			OUTC(dp, CS_SS2);
			OUTC(dp, c);
		} else if (c >= 0x81 && c <= 0xfc) {
			dp->sjis = 2;
			dp->pend = c;
		} else {
			OUTERR(dp, c);
		}
		dp->encoded = 1;
		return;
	}
	if (dp->sjis == 2) {				/* SJIS 2nd */
		s = dp->pend;
		s -= (s <= 0x9f) ? 0x71 : 0xb1;
		s = (s * 2 + 1) | 0x80;
		if (s >= 0xa0 && s <= 0xfd && c >= 0x9f && c <= 0xfc) {
			OUTC(dp, s + 1);
			OUTC(dp, c + 2);
		} else if (s >= 0xa1 && s <= 0xfe && c >= 0x40 && c <= 0x7e) {
			OUTC(dp, s);
			OUTC(dp, c + 0x61);
		} else if (s >= 0xa1 && s <= 0xfe && c >= 0x80 && c <= 0x9e) {
			OUTC(dp, s);
			OUTC(dp, c + 0x60);
		} else {
			OUTERR(dp, dp->pend);
			OUTERR(dp, c);
		}
		dp->pend = 0;
		dp->sjis = 1;
		return;
	}
	if (dp->gbk) {
		if (dp->pend) {
			dp->pend = (dp->pend << 8) | c;
			v = gbk_decode(dp->pend);
			goto euc;
		}
		if (c & 0x80) {
			dp->pend = c;
			return;
		}
	}
	if (dp->utf8) {
		if (c & 0x80) {
			dp->pend = (dp->pend << 8) | c;
			if ((dp->pend & 0xfff00000) == 0x00f00000 ||
			    (dp->pend & 0xffffe000) == 0x0000e000 ||
			    (dp->pend & 0xffffffc0) == 0x000000c0)
				return;
			v = utf8_decode(dp->pend);
  euc:
			dp->encoded = 1;
			if (v == 0) {
				OUTPEND(dp);
				return;
			}
			dp->pend = 0;
			c = v & 0xff;
			s = (v >> 8) & 0xff;
			if ((v >> 24) == CS_SSX) {
				OUTC(dp, CS_SSX);
				OUTC(dp, (v >> 16) & 0xff);
				OUTC(dp, s);
				OUTC(dp, c);
				return;
			}
			if ((v >> 16) == CS_SSX) {
				OUTC(dp, CS_SSX);
				OUTC(dp, s);
				OUTC(dp, c);
				return;
			}
			if ((v >> 16) == CS_SS3) {
				OUTC(dp, CS_SS3);
				OUTC(dp, s);
				OUTC(dp, c);
				return;
			}
			if ((v >> 8) == CS_SS2) {
				OUTC(dp, CS_SS2);
				OUTC(dp, c);
				return;
			}
			dp->pend = s;
#if 0		/* handle JIS text even if utf-8 is specified */
		} else {
			if (dp->pend)
				OUTPEND(dp);
#endif
		}
	}

	switch (dp->pend) {
	case 0:
		break;

	case 0x1b:
		switch (c) {
		case 0x24:	/* ESC-$- */
		case 0x28:	/* ESC-(- */
		case 0x29:	/* ESC-)- */
		case 0x2a:	/* ESC-*- */
		case 0x2b:	/* ESC-+- */
		case 0x2d:	/* ESC--- */
		case 0x2e:	/* ESC-.- */
		case 0x2f:	/* ESC-/- */
			dp->pend = (dp->pend << 8) | c;
			return;
		case 0x4e:	/* ESC-N */
			dp->pend = CS_SS2;
			return;
		case 0x4f:	/* ESC-O */
			dp->pend = CS_SS3;
			return;
		default:
			OUTPEND(dp);
			break;
		}
		break;

	case 0x1b24:
		switch (c) {
		case 0x28:	/* ESC-$-(- */
		case 0x29:	/* ESC-$-)- */
		case 0x2a:	/* ESC-$-*- */
		case 0x2b:	/* ESC-$-+- */
		case 0x2d:	/* ESC-$--- */
		case 0x2e:	/* ESC-$-.- */
		case 0x2f:	/* ESC-$-/- */
			dp->pend = (dp->pend << 8) | c;
			return;
		case 0x40:	/* ESC-$-@ */
		case 0x42:	/* ESC-$-B */
			dp->cscode[0] = CS_DWIDTH | CS_CODE('B');	/*XXX*/
			dp->pend = 0;
			return;
		default:
			OUTPEND(dp);
			break;
		}
		break;

	case 0x1b2428:		/* ESC-$-(- */
		if (c == '@')
			c = 'B';	/* XXX */
	case 0x1b2429:		/* ESC-$-)- */
	case 0x1b242a:		/* ESC-$-*- */
	case 0x1b242b:		/* ESC-$-+- */
	case 0x1b242d:		/* ESC-$--- */
	case 0x1b242e:		/* ESC-$-.- */
	case 0x1b242f:		/* ESC-$-/- */
		dp->cscode[dp->pend & 0x00000003] = CS_DWIDTH |
		    CS_CODE(c) | ((dp->pend & 0x00000004) ? CS_96SET : 0);
		dp->pend = 0;
		return;

	case 0x1b28:		/* ESC-(- */
		if (c == 'J')
			c = 'B';	/* XXX */
	case 0x1b29:		/* ESC-)- */
	case 0x1b2a:		/* ESC-*- */
	case 0x1b2b:		/* ESC-+- */
	case 0x1b2d:		/* ESC--- */
	case 0x1b2e:		/* ESC-.- */
	case 0x1b2f:		/* ESC-/- */
		dp->cscode[dp->pend & 0x0003] =
		    CS_CODE(c) | ((dp->pend & 0x0004) ? CS_96SET : 0);
		dp->pend = 0;
		return;

	case CS_SS2:
		G = 2;
		dp->pend = 0;
		break;

	case CS_SS3:
		G = 3;
		dp->pend = 0;
		break;

	default:
		if ((dp->pend & 0xff000000) == 0x1b000000 ||
		    (dp->pend & 0xffff0000) == 0x001b0000 ||
		    (dp->pend & 0xffffff00) == 0x00001b00) {
			OUTPEND(dp);
			break;
		}
		if ((G == 0 && (c & 0x80)) || dp->shift)
			G = 1;
		if (!(dp->cscode[G] & CS_DWIDTH)) {
			OUTPEND(dp);
			break;
		}
		if (dp->cscode[G] & CS_96SET) {
			if ((c & 0x7f) < 0x20) {
				OUTPEND(dp);
				break;
			}
		} else {
			if ((c & 0x7f) <= 0x20 || (c & 0x7f) >= 0x7f) {
				OUTPEND(dp);
				break;
			}
		}
		if (dp->skip) {
			;
		} else if (dp->cscode[G] == CS_KANJI) {
			if (G != 1 || !(dp->pend & 0x80) || !(c & 0x80))
				dp->encoded = 1;
			OUTC(dp, dp->pend | 0x80);
			OUTC(dp, c | 0x80);
		} else if (dp->cscode[G] == CS_HOJO) {
			dp->encoded = 1;
			OUTC(dp, CS_SS3);
			OUTC(dp, dp->pend | 0x80);
			OUTC(dp, c | 0x80);
		} else {
			dp->encoded = 1;
			OUTC(dp, CS_SSX);
			OUTC(dp, dp->cscode[G]);
			OUTC(dp, dp->pend | 0x80);
			OUTC(dp, c | 0x80);
		}
		dp->pend = 0;
		return;
	}

	if (c == 0x1b) {
		dp->pend = c;
		dp->encoded = 1;
	} else if (c == 0x0e) {
		dp->shift = 1;
		dp->encoded = 1;
	} else if (c == 0x0f) {
		dp->shift = 0;
		dp->encoded = 1;
	} else if (c == '\n' || c == '\t') {
		if (!dp->skip &&
		    (!dp->html || (dp->cnt>0 && (dp->buf[dp->cnt-1] != '\n'))))
			OUTC(dp, c);
	} else if (c == '\r') {
		dp->encoded = 1;
		dp->pend = '\r';
	} else if (c == '\014' /* ^L */) {
		if (!dp->skip) {
			OUTC(dp, '^'); OUTC(dp, c + '@');
		}
		dp->encoded = 1;
	} else if (c < ' ') {
		OUTERR(dp, c);
	} else {
		if ((G == 0 && (c & 0x80)) || dp->shift)
			G = 1;
		if (dp->cscode[G] & CS_96SET) {
			if ((c & 0x7f) < 0x20) {
				OUTERR(dp, c);
				return;
			}
		} else {
			if (c == ' ') {
				if (!dp->skip)
					OUTC(dp, c);
				return;
			}
			if ((c & 0x7f) <= 0x20 || (c & 0x7f) >= 0x7f) {
				OUTERR(dp, c);
				return;
			}
		}
		if (dp->cscode[G] & CS_DWIDTH) {
			dp->pend = c;
			return;
		}
		if (dp->skip) {
			;
		} else if (dp->cscode[G] == CS_ASCII) {
			if (G != 0 || (dp->pend & 0x80) || (c & 0x80))
				dp->encoded = 1;
			OUTC(dp, c);
		} else if (dp->cscode[G] == CS_KANA) {
			dp->encoded = 1;
			OUTC(dp, CS_SS2);
			OUTC(dp, c | 0x80);
		} else {
			dp->encoded = 1;
			OUTC(dp, CS_SSX);
			OUTC(dp, dp->cscode[G]);
			OUTC(dp, c | 0x80);
		}
	}
}

static void
save_dbuf_fp(struct decode *dp)
{

	if (dp->cnt > 0) {
		fwrite(dp->buf, dp->cnt, 1, dp->flsarg);
		dp->cnt = 0;
	}
}

/* decode 1 header */
int
decode_header(cbuf_t *hdr, int flags)
{
	char c;
	const char *p, *b, *ep;
	ccbuf_t charset;
	int i, code, skip;
	int qenc;
	static struct decode d;

	DINIT(&d, flags);
	qenc = 0;
	for (p = hdr->ptr, ep = p + hdr->len; p < ep; ) {
		if (*p == '\n') {
			b = p - 1;
			while (++p < ep) {
				if (*p != ' ' && *p != '\t')
					break;
			}
			if (p == ep)
				break;
			if (*b != '=' || *p != '=')
				DOUTC(&d, ' ');
			continue;
		}
		if (qenc) {
			if (*p == '?' && p + 1 < ep && p[1] == '=') {
				qenc = 0;
				p += 2;
				d.cscode[0] = CS_ASCII;
				d.shift = 0;
				d.pend = 0;
				continue;
			}
			if (*p == '=') {
				if (p + 1 < ep && p[1] == '\n') {
					p += 2;	/* continuation */
					continue;
				}
				if (p + 2 < ep) {
					p++;
					c = rhex[*(unsigned char *)p++] << 4;
					c |= rhex[*(unsigned char *)p++];
					DOUTC(&d, c);
					continue;
				}
			}
			if (*p == '_') {
				p++;
				DOUTC(&d, ' ');
				continue;
			}
		}
		if (*p != '=' || p + 7 > ep || p[1] != '?') {
  nodecode:
			DOUTC(&d, *p);
			p++;
			continue;
		}

		for (CSETP(&charset, p + 2); ; CL(&charset)++) {
			if (CE(&charset) + 4 >= ep)
				goto nodecode;
			if (CE(&charset)[0] == '?' &&
			    (CE(&charset)[1]=='B' || CE(&charset)[1]=='Q' ||
			     CE(&charset)[1]=='b' || CE(&charset)[1]=='q') &&
			    CE(&charset)[2] == '?')
				break;
		}
		p = CE(&charset) + 3;
		for (i = 0; i < CL(&charset); i++) {
			if (CP(&charset)[i] == '*') {
				CL(&charset) = i;
				break;
			}
		}
		d.cscode[0] = CS_ASCII;
		code = charset_id(&charset);
		if (code == FDB_CS_DEFAULT)
			goto nodecode;
		charset_dinit(&d, code);
		d.encoded = 1;
		d.shift = 0;
		d.pend = 0;
		if (p[-2] == 'Q' || p[-2] == 'q') {
			qenc = 1;
			continue;
		}
		for (;;) {
			if (p + 1 < ep && *p == '?' && p[1] == '=') {
				p += 2;
				d.cscode[0] = CS_ASCII;
				d.shift = 0;
				d.pend = 0;
				break;
			}
			for (i = 0, code = 0, skip = 0; i < 4; i++, p++) {
				code <<= 6;
  nextline:
				if (p >= ep)
					goto error;
				while (*p == ' ' || *p == '\t') {
					p++;
					if (p >= ep)
						goto error;
				}
				if (*p == '=') {
					skip += 6;
				} else {
					c = rb64[*(u_char *)p];
					if (c == -1) {
						if (*p++ == '\n')
							goto nextline;
						goto error;
					}
					code |= c;
				}
			}
			for (i = 8; i + skip <= 24; i += 8) {
				c = (code >> 16) & 0xff;
				DOUTC(&d, c);
				code <<= 8;
			}
		}
	}
	if (d.encoded) {
		hdr->ptr = d.buf;
		hdr->len = d.cnt;
		return (d.err ? -1 : 1);
	}
  error:
	return 0;
}

int
decode_param(ccbuf_t *param, int seq, int ext)
{
	char c;
	const char *p, *ep;
	int code;
	ccbuf_t charset;
	static struct decode d;

	p = param->ptr;
	ep = p + param->len;
	if (seq == 0) {
		code = 0;
		if (ext) {
			CSETP(&charset, p);
			for (; p < ep; p++) {
				if (*p == '\'') {
					CSETE(&charset, p);
					code = charset_id(&charset);
					p++;
					while (p < ep) {
						if (*p++ == '\'')
							break;
					}
					break;
				}
			}
		}
		DINIT(&d, code);
	}
	while (p < ep) {
		if (ext && *p == '%' && p + 2 < ep) {
			p++;
			c = rhex[*(unsigned char *)p++] << 4;
			c |= rhex[*(unsigned char *)p++];
		} else
			c = *p++;
		DOUTC(&d, c);
	}
	if (d.encoded) {
		param->ptr = d.buf;
		param->len = d.cnt;
		return (d.err ? -1 : 1);
	}
	return 0;
}

/* decode 1 line body */
static void
decode_qenc(struct decode *dp, const char **pp, const char *ep)
{
	char c;
	const char *p;

	for (p = *pp, c = '\n'; p < ep; ) {
		if ((c = *p++) == '\n') {
			DOUTC(dp, c);
			if (dp->noconv || dp->skip)
				continue;
			break;
		}
		if (c == '=') {
			dp->encoded = 1;
			if (p < ep && *p == '\n') {
				p++;	/* continuation */
				continue;
			}
			if (p + 3 >= ep)
				break;
			c = rhex[*(unsigned char *)p++] << 4;
			c |= rhex[*(unsigned char *)p++];
		}
		DOUTC(dp, c);
	}
	if (c != '\n') {
		while (p < ep) {
			if (*p++ == '\n')
				break;
		}
	}
	*pp = p;
	return;
}

/* decode base64 text */
static void
decode_base64(struct decode *dp, const char **pp, const char *ep)
{
	int c;
	struct b64_state bs;

	if (*pp < ep)
		dp->encoded = 1;
	b64_set(&bs, *pp, ep);
	for (;;) {
		c = b64_getc(&bs);
		if (c == -1)
			break;
		DOUTC(dp, c);
		if (!dp->noconv && c == '\n') {
			if (dp->cnt > 0 && c == '\n' && dp->sjis <= 1 &&
			    dp->pend == 0 && !b64_hold(&bs))
				break;
		}
	}
	*pp = b64_getptr(&bs);
}

/* decode uuencode text */
static void
decode_uuencode(struct decode *dp, const char **pp, const char *ep)
{
	char c;
	const char *p, *s;
	int i, code, cnt;

	p = *pp;
	if (p < ep)
		dp->encoded = 1;
	while (p < ep) {
		if (!dp->noconv && dp->cnt > 0 && dp->buf[dp->cnt-1] == '\n') {
			if (*p == '\n')
				p++;
			break;
		}
		cnt = ruue[*(unsigned char *)p];
		if (cnt < 0) {
			s = p;
			while (p < ep) {
				if (*p++ == '\n')
					break;
			}
			if (s + 3 < p && strncmp(s, "end", 3) == 0)
				break;
			continue;
		}
		p++;
		while (cnt > 0) {
			for (i = 0, code = 0; i < 4; i++, p++) {
				if (p >= ep)
					goto error;
				code <<= 6;
				c = ruue[*(unsigned char *)p];
				if (c < 0)
					goto error;
				code |= c;
			}
			for (i = 0; i < 3; i++) {
				if (cnt-- == 0)
					break;
				c = (code >> 16) & 0xff;
				DOUTC(dp, c);
				code <<= 8;
			}
		}
		if (*p != '\n')
			break;
		p++;
	}
  error:
	while (p < ep) {
		if (*p++ == '\n')
			break;
	}
	*pp = p;
}

/* decode binhex text */
/* XXX */
static void
decode_binhex(struct decode *dp, const char **pp, const char *ep)
{
	const char *p;

	dp->encoded = 1;

	/* start */
	p = *pp;
	if (*p != ':') {
		while (p < ep) {
			if (*p++ == '\n')
				break;
		}
		*pp = p;
		return;
	}
	/* end */
	while (p < ep) {
		if (*p++ == ':')
			break;
	}
	*pp = p;
	for (p = banner("BINHEX"); *p != '\0'; p++) {
		OUTC(dp, *p);
	}
	OUTC(dp, '\n');
}

/* decode zip text */
/* XXX */
static void
decode_zip(struct decode *dp, const char **pp, const char *ep)
{
	int i, c, len;
	char *p;
	struct zip_hdr zh;
	struct b64_state bs;

	dp->encoded = 1;
	for (p = banner("ZIP"); *p != '\0'; p++) {
		OUTC(dp, *p);
	}
	OUTC(dp, '\n');

	dp->sjis = 1;
	b64_set(&bs, *pp, ep);
	for (;;) {
		if (b64_ngetc(&bs, (char *)&zh, sizeof(zh)) != sizeof(zh))
			goto error;
		if (GET_LE4(zh.zh_magic) != ZIP_MAGIC) {
			b64_ngetc(&bs, NULL, ep - *pp);
			break;
		}
		len = GET_LE2(zh.zh_fnlen);
		for (i = 0; i < len; i++) {
			if ((c = b64_getc(&bs)) == -1)
				goto error;
			DOUTC(dp, c);
		}
		DOUTC(dp, '\n');
		len = GET_LE2(zh.zh_exlen) + GET_LE4(zh.zh_csize);
		if (b64_ngetc(&bs, NULL, len) != len)
			goto error;
	}
  error:
	*pp = b64_getptr(&bs);
}

#ifdef USE_ZLIB

/* decode x-gzip64 text */
static void
decode_gzip64(struct decode *dp, const char **pp, const char *ep)
{
	char *p;
	int c, cc, total;
	struct b64_state bs;
	struct gzip_hdr gz;
	z_stream z;
	char bbuf[CHARBLOCK], dbuf[CHARBLOCK];

	dp->encoded = 1;
	b64_set(&bs, *pp, ep);
	if (b64_ngetc(&bs, (char *)&gz, sizeof(gz)) != sizeof(gz))
		goto error;
	if (GET_LE2(gz.gz_magic) != GZIP_MAGIC || gz.gz_method != Z_DEFLATED)
		goto error;
	if (gz.gz_flags & GZF_EXTRA) {
		if (b64_ngetc(&bs, dbuf, 2) != 2)
			goto error;
		cc = GET_LE2(dbuf);
		b64_ngetc(&bs, NULL, cc);
	}
	if (gz.gz_flags & GZF_NAME) {
		while ((c = b64_getc(&bs)) != '\0') {
			if (c == -1)
				goto error;
		}
	}
	if (gz.gz_flags & GZF_COMMENT) {
		while ((c = b64_getc(&bs)) != '\0') {
			if (c == -1)
				goto error;
		}
	}
	if (gz.gz_flags & GZF_HDRCRC) {
		if (b64_ngetc(&bs, NULL, 2) != 2)
			goto error;
	}

	memset(&z, 0, sizeof(z));
	z.next_in = (Bytef *)bbuf;
	z.avail_in = 0;
	z.next_out = (Bytef *)dbuf;
	z.avail_out = sizeof(dbuf);
	if (inflateInit2(&z, -15) != Z_OK)
		goto error;
	total = 0;
	for (;;) {
		if (z.avail_in == 0)
			z.avail_in = b64_ngetc(&bs, bbuf, sizeof(bbuf));
		cc = inflate(&z, Z_SYNC_FLUSH);
		if (cc == Z_STREAM_END)
			break;
		if (cc < 0) {
			inflateEnd(&z);
			goto error;
		}
		if (z.avail_out == 0) {
			for (p = dbuf; p < dbuf + sizeof(dbuf); p++)
				DOUTC(dp, *p);
			total = z.total_out;
			z.next_out = (Bytef *)dbuf;
			z.avail_out = sizeof(dbuf);
		}
	}
	for (p = dbuf; total < z.total_out; total++, p++)
		DOUTC(dp, *p);
	inflateEnd(&z);
	*pp = b64_getptr(&bs);
	return;
 error:
	dp->noconv = 1;
	decode_base64(dp, pp, ep);
}
#endif /* USE_ZLIB */

/* decode 1 line body */
int
decode_text(const char **pp, const char *ep, cbuf_t *res, int flags)
{
	char c;
	const char *p;
	char *q;
	static struct decode d;
	cbuf_t line, vcal;
	int retval;

	vcal.ptr = NULL;
	vcal.len = 0;
  again:
	DINIT(&d, flags);
	if (flags & FDB_HTML)
		d.html = 1;
	res->ptr = (char *)*pp;

	switch (flags & FDB_ENCODE) {
	case FDB_ENC_Q:
		decode_qenc(&d, pp, ep);
		ep = *pp;
		break;
	case FDB_ENC_B64:
		decode_base64(&d, pp, ep);
		ep = *pp;
		break;
#ifdef USE_ZLIB
	case FDB_ENC_GZ64:
		decode_gzip64(&d, pp, ep);
		ep = *pp;
		break;
#endif /* USE_ZLIB */
	case FDB_ENC_UU:
		decode_uuencode(&d, pp, ep);
		ep = *pp;
		break;
	case FDB_ENC_BINHEX:
		decode_binhex(&d, pp, ep);
		ep = *pp;
		break;
	case FDB_ENC_ZIP:
		decode_zip(&d, pp, ep);
		ep = *pp;
		break;
	default:
		for (p = *pp; p < ep; ) {
			if ((c = *p++) == '\n' && !d.skip)
				break;
			DOUTC(&d, c);
		}
		*pp = p;
		break;
	}
	if (d.encoded || (flags & FDB_CALENDAR)) {
		res->ptr = d.buf;
		res->len = d.cnt;
		retval = d.err ? -1 : 1;
	} else {
		res->len = *pp - res->ptr;
		retval = 0;
	}
	if (flags & FDB_CALENDAR) {
		CSUBST("ROLE=REQ-PARTICIPANT;", "REQ;", res);
		CSUBST("ROLE=OPT-PARTICIPANT;", "OPT;", res);
		CSUBST("PARTSTAT=NEEDS-ACTION;", NULL, res);
		CSUBST("RSVP=TRUE;", NULL, res);
		CSUBST("LANGUAGE=ja-JP:", NULL, res);
		CSUBST("TZID=Tokyo Standard Time:", NULL, res);
		CSUBST(";CN=", ";\t", res);
		CSUBST(":MAILTO:", "\t", res);
		while (CL(res) > 0) {
			q = CP(res);
			CSETP(&line, q);
			while (CL(res) > 0) {
				q = CP(res);
				CADDP(res, 1);
				if (*q == '\n') {
					if (CL(res) == 0 || !isspace((unsigned char)q[1]))
						break;
					CADDP(res, 1);
					memmove(q, CP(res), CL(res));
					CP(res) = q;
				} else if (*q == '\\' && CL(res) > 0) {
					c = q[1];
					if (c == 'n')
						*q++ = '\n';
					else if (c == 't')
						*q++ = '\t';
					else
						*q++ = c;
					CADDP(res, 1);
					memmove(q, CP(res), CL(res));
					CP(res) = q;
				}
			}
			CSETE(&line, q);
			if (CMATCH("BEGIN:VEVENT", &line)) {
				CSETP(&vcal, CP(res));
			} else if (CMATCH("END:VEVENT", &line)) {
				CSETE(&vcal, CP(&line));
			}
		}
		if (*pp < ep)
			goto again;
		if (vcal.ptr != NULL)
			*res = vcal;
	}
	return retval;
}

int
charset_id(const ccbuf_t *charset)
{
	int i, j, len;
	const char *p;

	for (i = 0; charset_tbl[i].cs_name != NULL; i++) {
		if (i == FDB_CS_DEFAULT)
			continue;
		if (CL(charset) == charset_tbl[i].cs_len &&
		    strncasecmp(CP(charset), charset_tbl[i].cs_name,
		    CL(charset)) == 0)
			return i;
		for (j = 0; j < sizeof(charset_tbl[i].cs_alias) /
		    sizeof(charset_tbl[i].cs_alias[j]); j++) {
			p = charset_tbl[i].cs_alias[j];
			if (p != NULL && (len = strlen(p)) != 0 &&
			    CL(charset) == len &&
			    strncasecmp(CP(charset), p, len) == 0)
				return i;
		}
	}
	return FDB_CS_DEFAULT;
}

const char *
charset_name(int id)
{

	if (id < 0 && id >= sizeof(charset_tbl)/sizeof(charset_tbl[0]))
		id = 0;
	return charset_tbl[id].cs_name;
}

int
charset_completion(char *candidate, void (*callback)(void *arg, const char *entry), void *arg)
{
	const char *cs;
	int id, i, len, nlen;
	int nmatch = 0, matchlen = 0;
	char mname[64];

	len = strlen(candidate);
	mname[0] = '\0';
	for (id = 0; id < sizeof(charset_tbl) / sizeof(charset_tbl[0]); id++) {
		cs = charset_tbl[id].cs_name;
		if (cs == NULL)
			continue;
		nlen = strlen(cs);
		if (nlen >= len &&
		    memcmp(cs, candidate, len) == 0) {
			if (nmatch && mname[0]) {
				for (i = len; i < matchlen; i++) {
					if (i >= nlen || cs[i] != mname[i]) {
						matchlen = i;
						break;
					}
				}
			} else {
				matchlen = nlen;
			}
			nmatch++;
			if (callback)
				callback(arg, cs);
			strlcpy(mname, cs, sizeof(mname));
		}
	}
	if (nmatch && matchlen > len) {
		memcpy(candidate + len, mname + len, matchlen - len);
		candidate[matchlen] = '\0';
	}
	return nmatch;
}

int
charset_guess(const char *p, const char *ep)
{
	int i, code, id;
	static struct decode d;

	DINIT(&d, 0);
	d.cscode[1] = d.cscode[2] = d.cscode[3] = code = CS_ASCII;
	for (; p < ep; p++) {
		if (*p & 0x80) {
			/* XXX assume 8bit is used only for euc-jp */
			if (p + 1 == ep || (p[1] & 0x80) == 0)
				code = CS_96SET|CS_CODE('A');	/* 8859-1 */
			else
				code = CS_KANJI;		/* 2022-jp */
			break;
		}
		DOUTC(&d, *p);
		for (i = 0; i < 4; i++) {
			if (d.cscode[i] != CS_ASCII && d.cscode[i] != code) {
				if (code != CS_ASCII)
					return FDB_CS_JP2;
				code = d.cscode[i];
				break;
			}
		}
	}
	if (code == CS_ASCII)
		return FDB_CS_ASCII;
	for (id = 0; charset_tbl[id].cs_name != NULL; id++) {
		for (i = 0; i < 3; i++) {
			if (code == charset_tbl[id].cs_code[i])
				return id;
		}
	}
	return FDB_CS_JP2;
}

/* decode base64 binary */
static int
save_base64_decode(FILE *fp, ccbuf_t *buf)
{
	int c;
	struct b64_state bs;

	b64_set(&bs, CP(buf), CE(buf));
	for (;;) {
		c = b64_getc(&bs);
		if (c == -1)
			return 0;
		putc((u_char)c, fp);
	}
	return 1;
}

/* decode binhex binary */
static int
save_binhex(FILE *fp, ccbuf_t *buf)
{
	int i, code;
	int st;
	char c, prev;
	const char *p, *ep;
	int rcnt, ccnt, slen;
	static int len[] = {
		1,	/* file name len */
		0,	/* file name (MODIFIED) */
		1 + 4 + 4 + 2,	/* skip: vers, type, creator, flags */
		4,	/* data length */
		4,	/* resource length */
		2,	/* crc */
		0,	/* data (MODIFIED) */
		2,	/* data crc */
		0,	/* resource (MODIFIED) */
		2,	/* resource crc */
		-1,	/* end of format */
	};

	p = buf->ptr;
	ep = p + buf->len;

	/* seek ":" in column 1 */
	while (p < ep) {
		if (*p == ':') {
			p++;
			break;
		}
		while (p < ep) {
			if (*p++ == '\n')
				break;
		}
	}

	code = 0;	/* for debug */
	st = 0;
	rcnt = 0;
	ccnt = 0;
	prev = 0;
	slen = 0;
	for (;;) {
		if (rcnt > 0) {
			rcnt--;
			c = prev;
		} else {
			if (ccnt == 0) {
				for (i = 0, code = 0; i < 4; i++, p++) {
					for (;;) {
						if (p >= ep)
							goto error;
						if (*p != '\n')
							break;
						p++;
					}
					code <<= 6;
					c = rhqx[*(unsigned char *)p];
					if (c < 0)
						goto error;
					code |= c;
				}
				ccnt = 3;
			}
			c = (code >> (--ccnt * 8)) & 0xff;
			if (rcnt < 0) {
				if (c) {
					rcnt = c - 1;
					continue;
				}
				rcnt = 0;
				c = 0x90;
			} else if ((u_char)c == 0x90) {
				rcnt = -1;
				continue;
			}
		}
		switch (st) {
		case 0:		/* file name length */
			len[1] = c;
			break;
		case 3:		/* data length */
			len[6] |= (u_char)c << ((3 - slen) * 8);
			break;
		case 4:		/* resource length */
			len[8] |= (u_char)c << ((3 - slen) * 8);
			break;
		case 6:		/* data */
			putc((u_char)c, fp);
			break;
		}
		if (++slen == len[st]) {
			slen = 0;
			while (len[++st] == 0)
				;
			if (len[st] < 0) {
				if (p < ep && *p == ':')
					return 1;
				break;
			}
		}
	}
  error:
	return 0;
}

/* decode zip binary */
static int
save_single_zip(FILE *ofp, ccbuf_t *buf)
{
	char *tmpdir, *tmpfile;
	int fd, len, wstatus;
	FILE *fp;
	int pfds[2];
	char fname[MAXPATH];
	char obuf[BUFSIZ];

	if ((tmpdir = getenv("TMPDIR")) == NULL)
		tmpdir = "/tmp";
	tmpfile = fname;
	snprintf(fname, sizeof(fname), "%s/cue.XXXXXX", tmpdir);
#ifdef HAVE_MKSTEMP
	fd = mkstemp(fname);
#else /* HAVE_MKSTEMP */
	fd = open(mktemp(fname), O_CREAT|O_EXCL|O_RDWR);
#endif /* HAVE_MKSTEMP */
	if (fd < 0)
		goto end2;
	fp = fdopen(fd, "w");
	if (fp == NULL) {
		close(fd);
		goto end;
	}
	save_base64_decode(fp, buf);
	fclose(fp);
	if (pipe(pfds) < 0)
		goto end;
	if (proc_exec(PTYPE_TEST, -1, pfds[1], NULL, NULL, "unzip -p %s", fname))
		goto end;
	close(pfds[1]);
	while ((len = read(pfds[0], obuf, sizeof(obuf))) > 0)
		fwrite(obuf, len, 1, ofp);
	close(pfds[0]);
	if (proc_getstate(PTYPE_TEST, &wstatus, NULL, 0) != PTYPE_TEST ||
	    WIFSIGNALED(wstatus) || WEXITSTATUS(wstatus) != 0) {
		goto end;
	}

  end:
	unlink(fname);
  end2:
	return 1;
}

void
save_part(struct state *state, int all)
{
	struct filedb *fdb;
	FILE *fp;
	char buf[CHARBLOCK];
	char *p, *ep;
	const char *cp, *cep;
	ccbuf_t *cb;
	struct stat stbuf;
	int i;
	int singlezip = 0;
	static struct decode d;
	struct utimbuf utm;

	if ((fdb = state->message) == NULL)
		return;
	message_parseall(fdb);

#ifdef USE_ZLIB
	if (!all && (fdb->flags & FDB_ENCODE) == FDB_ENC_ZIP) {
		if (fdb->lines - fdb->hdr_lines == 7) {
			fdb->filename = *fdb_read(fdb, fdb->lines - 1);
			singlezip = 1;
		}
	}
#endif /* USE_ZLIB */

	p = state->status;
	ep = state->status + sizeof(state->status);
	strlcpy(p, "File", ep - p);
	p += 4;
	if (fdb->filename.ptr) {
		i = snprintf(p, ep - p, " (%.*s)", fdb->filename.len,
		    fdb->filename.ptr);
		if (i <= 0) {
			strlcpy(state->status, "Quit", sizeof(state->status));
			return;
		}
		p += i;
	}
	strlcpy(p, ": ", ep - p);
	strlcpy(buf, "~/", sizeof(buf));
	if (edit_stline(state, buf, sizeof(buf), fname_completion) == NULL) {
		strlcpy(state->status, "Quit", sizeof(state->status));
		return;
	}
	if (buf[0] == '\0')
		strlcpy(buf, ".", sizeof(buf));
	else
		fname_expand(buf, buf);
	if (stat(buf, &stbuf) == 0 && S_ISDIR(stbuf.st_mode)) {
		if (fdb->filename.ptr) {
			sprintf(buf + strlen(buf), "/%.*s",
				fdb->filename.len, fdb->filename.ptr);
		} else {
			errno = EISDIR;
			snprintf(state->status, sizeof(state->status),
			     "%s: %s", buf, strerror(errno));
			return;
		}
	}
	if (stat(buf, &stbuf) == 0) {
		if (!edit_yn(state, "%s: overwrite? ", buf)) {
			strlcpy(state->status, "Quit", sizeof(state->status));
			return;
		}
	}
	if ((fp = fopen(buf, "w")) == NULL) {
		snprintf(state->status, sizeof(state->status),
		    "%s: %s", buf, strerror(errno));
		return;
	}
	if (all) {
		switch (fdb->flags & FDB_ENCODE) {
		case FDB_ENC_B64:
		case FDB_ENC_ZIP:
			save_base64_decode(fp, &fdb->buf_body);
			break;
		default:
			cp = CP(&fdb->buf_mhdr);
			cep = CE(&fdb->buf_body);
			fwrite(cp, cep - cp, 1, fp);
			break;
		}
	} else if ((fdb->flags & FDB_NOTEXT) && !(fdb->flags & FDB_INLINE)) {
		DINIT(&d, FDB_NOCONV);
		d.flsbuf = save_dbuf_fp;
		d.flsarg = fp;
		cp = CP(&fdb->buf_body);
		cep = CE(&fdb->buf_body);

		switch (fdb->flags & FDB_ENCODE) {
		case FDB_ENC_Q:
			decode_qenc(&d, &cp, cep);
			break;
		case FDB_ENC_B64:
			decode_base64(&d, &cp, cep);
			break;
		case FDB_ENC_UU:
			decode_uuencode(&d, &cp, cep);
			break;
		case FDB_ENC_BINHEX:
			save_binhex(fp, &fdb->buf_body);
			break;
		case FDB_ENC_ZIP:
			if (singlezip)
				save_single_zip(fp, &fdb->buf_body);
			else
				decode_base64(&d, &cp, cep);
			break;
#ifdef USE_ZLIB
		case FDB_ENC_GZ64:
			decode_gzip64(&d, &cp, cep);
			break;
#endif /* USE_ZLIB */
		default:
			fwrite(cp, cep - cp, 1, fp);
			break;
		}
		OFLUSH(&d);
	} else {
		for (i = fdb->skip_lines; (cb = fdb_read(fdb, i)) != NULL; i++) {
			print_jis(fp, "%.*s%s", CL(cb), CP(cb),
				  (fdb_ismore(fdb, i) ? "" : "\n"));
		}
	}
	fclose(fp);
	if (fdb->filetime > 0) {
		utm.actime = utm.modtime = fdb->filetime;
		utime(buf, &utm);
	}

	snprintf(state->status, sizeof(state->status), "Saved to %s", buf);
}

void
print_jis(FILE *fp, const char *fmt, ...)
{
	va_list ap;
	u_char c, cs, *p;
	u_char cscode[4];
	u_char *buf, buf0[CHARBLOCK];
	int len;

	va_start(ap, fmt);
	len = vsnprintf((char *)buf0, sizeof(buf0), fmt, ap);
	va_end(ap);
	if (len < sizeof(buf0))
		buf = buf0;
	else {
		buf = malloc(++len);
		va_start(ap, fmt);
		vsnprintf((char *)buf, len, fmt, ap);
		va_end(ap);
	}

	cscode[0] = CS_ASCII;
	cscode[1] = cscode[2] = cscode[3] = 0;

	p = buf;
	for (;;) {
		c = *p++;
		switch (c) {
		case CS_SS2:
			c = *p++;
			if (c == '\0')
				goto exit;
			if (cscode[0] != CS_KANA) {
				putc('\033', fp);
				putc('(', fp);
				putc('I', fp);
				cscode[0] = CS_KANA;
			}
			c &= ~0x80;
			putc(c, fp);
			break;

		case CS_SS3:
			c = *p++;
			if (c == '\0' || *p == '\0')
				goto exit;
			if (cscode[0] != CS_HOJO) {
				putc('\033', fp);
				putc('$', fp);
				putc('(', fp);
				putc('D', fp);
				cscode[0] = CS_HOJO;
			}
			c &= ~0x80;
			putc(c, fp);
			c = *p++ & ~0x80;
			putc(c, fp);
			break;

		case CS_SSX:
			cs = *p++;
			if (cs == '\0' || *p == '\0' ||
			    ((cs & CS_DWIDTH) && p[1] == '\0'))
				goto exit;
			if (cs & CS_96SET) {
				if (cs != cscode[2]) {
					putc('\033', fp);
					if (cs & CS_DWIDTH)
						putc('$', fp);
					putc('.', fp);
					putc('@' + CS_CODE(cs), fp);
					cscode[2] = cs;
				}
			} else {
				if (cs != cscode[0]) {
					putc('\033', fp);
					if (cs & CS_DWIDTH)
						putc('$', fp);
					putc('(', fp);
					putc('@' + CS_CODE(cs), fp);
					cscode[0] = cs;
				}
			}
			c = *p++ & ~0x80;
			putc(c, fp);
			if (cs & CS_DWIDTH) {
				c = *p++ & ~0x80;
				putc(c, fp);
			}
			break;

		default:
			if (c & 0x80) {
				if (*p == '\0') {
					c = '\0';
					goto exit;
				}
				if (cscode[0] != CS_KANJI) {
					putc('\033', fp);
					putc('$', fp);
					putc('B', fp);
					cscode[0] = CS_KANJI;
				}
				c &= ~0x80;
				putc(c, fp);
				c = *p++ & ~0x80;
				putc(c, fp);
			} else {
  exit:
				if (cscode[0] != CS_ASCII) {
					putc('\033', fp);
					putc('(', fp);
					putc('B', fp);
					cscode[0] = CS_ASCII;
				}
				if (c == '\0')
					goto end;
				putc(c, fp);
			}
		}
	}
  end:
	if (buf != buf0)
		free(buf);
}


/* XXX should be merged with print_jis() */
static int
encode_hdr(u_char **pp, u_char *ep, cbuf_t *res, int limit)
{
	const u_char *csname;
	u_char *s, *p;
	u_char c;
	int id;
	int elen;
	u_char qbuf[4];
	u_char cs;
	u_char cscode[4];
	static struct decode d;

	cscode[0] = CS_ASCII;
	cscode[1] = cscode[2] = cscode[3] = 0;

	id = charset_guess((char *)*pp, (char *)ep);
	if (id == FDB_CS_ASCII) {
		/* should not happen */
		res->ptr = (char *)*pp;
		res->len = ep - *pp;
		*pp = ep;
		return res->len;
	}
	DINIT(&d, id);

	csname = (u_char *)charset_name(id);
	limit -= 2 + strlen((char *)csname) + 3 + 2;
	if (limit < 4 + 4 + 4)	/* esc + word + esc */
		return 0;

	OUTC(&d, '='); OUTC(&d, '?');
	for (; *csname; csname++)
		OUTC(&d, *csname);
	OUTC(&d, '?'); OUTC(&d, charset_tbl[id].cs_enc); OUTC(&d, '?');
	if (charset_tbl[id].cs_enc == 'Q') {
		for (p = *pp; p < ep; p++) {
			if (*p == CS_SSX) {
				c = p[2] | 0x80;
				snprintf((char *)qbuf, sizeof(qbuf), "%02x", c);
				p += 2;
				OUTC(&d, '=');
				OUTC(&d, qbuf[0]);
				OUTC(&d, qbuf[1]);
			} else if (*p == '=') {
				OUTC(&d, '=');
				OUTC(&d, '3');
				OUTC(&d, 'd');
			} else
				OUTC(&d, *p);
		}
		OUTC(&d, '?'); OUTC(&d, '=');
		res->ptr = d.buf;
		res->len = d.cnt;
		elen = p - *pp;
		*pp = p;
		return elen;
	}
	elen = 0;
	limit = limit * 3 / 4;		/* B encoding */
	for (s = p = *pp; p < ep; s = p) {
		c = *p++;

		switch (c) {
		case '\0':
			goto exit;

		case CS_SS2:
			c = *p++;
			if (c == '\0')
				goto exit;
			if (++elen > limit)
				goto exit;
			if (cscode[0] != CS_KANA) {
				if (cscode[0] == CS_ASCII)
					limit -= 3;
				if ((elen += 3) > limit)
					goto exit;
				BOUTC(&d, '\033');
				BOUTC(&d, '(');
				BOUTC(&d, 'I');
				cscode[0] = CS_KANA;
			}
			c &= ~0x80;
			BOUTC(&d, c);
			break;

		case CS_SS3:
			c = *p++;
			if (c == '\0' || *p == '\0')
				goto exit;
			if ((elen += 2) > limit)
				goto exit;
			if (cscode[0] != CS_HOJO) {
				if (cscode[0] == CS_ASCII)
					limit -= 3;
				if ((elen += 4) > limit)
					goto exit;
				BOUTC(&d, '\033');
				BOUTC(&d, '$');
				BOUTC(&d, '(');
				BOUTC(&d, 'D');
				cscode[0] = CS_HOJO;
			}
			c &= ~0x80;
			BOUTC(&d, c);
			c = *p++ & ~0x80;
			BOUTC(&d, c);
			break;

		case CS_SSX:
			cs = *p++;
			if (cs == '\0' || *p == '\0' ||
			    ((cs & CS_DWIDTH) && p[1] == '\0'))
				goto exit;
			if ((elen += (cs & CS_DWIDTH) ? 2 : 1) > limit)
				goto exit;
			if (cs & CS_96SET) {
				if ((elen += 2) > limit)
					goto exit;
				if (cs != cscode[2]) {
					elen += (cs & CS_DWIDTH) ? 4 : 3;
					if (elen > limit)
						goto exit;
					BOUTC(&d, '\033');
					if (cs & CS_DWIDTH)
						BOUTC(&d, '$');
					BOUTC(&d, '.');
					BOUTC(&d, '@' + CS_CODE(cs));
					cscode[2] = cs;
				}
			} else {
				if (cs != cscode[0]) {
					if (cscode[0] == CS_ASCII)
						limit -= 3;
					elen += (cs & CS_DWIDTH) ? 4 : 3;
					if (elen > limit)
						goto exit;
					BOUTC(&d, '\033');
					if (cs & CS_DWIDTH)
						BOUTC(&d, '$');
					BOUTC(&d, '(');
					BOUTC(&d, '@' + CS_CODE(cs));
					cscode[0] = cs;
				}
			}
			c = *p++ & ~0x80;
			BOUTC(&d, c);
			if (cs & CS_DWIDTH) {
				c = *p++ & ~0x80;
				BOUTC(&d, c);
			}
			break;

		default:
			if (c & 0x80) {
				if (*p == '\0')
					goto exit;
				if ((elen += 2) > limit)
					goto exit;
				if (cscode[0] != CS_KANJI) {
					if (cscode[0] == CS_ASCII)
						limit -= 3;
					if ((elen += 3) > limit)
						goto exit;
					BOUTC(&d, '\033');
					BOUTC(&d, '$');
					BOUTC(&d, 'B');
					cscode[0] = CS_KANJI;
				}
				c &= ~0x80;
				BOUTC(&d, c);
				c = *p++ & ~0x80;
				BOUTC(&d, c);
			} else {
				if (++elen > limit)
					goto exit;
				if (cscode[0] != CS_ASCII) {
					limit += 3;
					if ((elen += 3) > limit)
						goto exit;
					BOUTC(&d, '\033');
					BOUTC(&d, '(');
					BOUTC(&d, 'B');
					cscode[0] = CS_ASCII;
				}
				BOUTC(&d, c);
			}
		}
	}
  exit:
	if (cscode[0] != CS_ASCII) {
		BOUTC(&d, '\033');
		BOUTC(&d, '(');
		BOUTC(&d, 'B');
		cscode[0] = CS_ASCII;
	}
	BFLUSH(&d);
	OUTC(&d, '?'); OUTC(&d, '=');
	res->ptr = d.buf;
	res->len = d.cnt;
	elen = s - *pp;
	*pp = s;
	return elen;
}

void
print_hdr(FILE *fp, const char *fmt, ...)
{
	va_list ap;
	u_char *p, *s, *t, quote;
	int width, slen;
	int encode, prevenc;
	cbuf_t cbuf;
	u_char buf[CHARBLOCK];
	static struct decode d;
	int fold = 78;

	va_start(ap, fmt);
	vsnprintf((char *)buf, sizeof(buf), fmt, ap);
	va_end(ap);

#if 0
	if (strncasecmp(buf, "Content-", 8) == 0
	||  strncasecmp(buf, "To:", 3) == 0
	||  strncasecmp(buf, "Cc:", 3) == 0
	||  strncasecmp(buf, "From:", 5) == 0
	||  strncasecmp(buf, "Sender:", 7) == 0
	||  strncasecmp(buf, "Apparently-To:", 14) == 0
	||  strncasecmp(buf, "Resent-To:", 10) == 0
	||  strncasecmp(buf, "Resent-Cc:", 10) == 0
	||  strncasecmp(buf, "Resent-From:", 12) == 0) {
		sflag = 1;
	} else {
		sflag = 0;
	}
#endif

	/* convert euc if jis code */
	for (p = buf; *p; p++) {
		if (*p == '\033') {
			/* would be JIS text -- convert euc */
			DINIT(&d, 0);
			for (p = buf; *p; p++)
				DOUTC(&d, *p);
			memcpy(buf, d.buf, d.cnt);
			buf[d.cnt] = '\0';
			break;
		}
	}

	width = 0;
	quote = 0;
	prevenc = 0;
	p = buf;
	while (*p) {
		/* get space */
		for (s = p; *p; p++) {
			if (!isspace(*p))
				break;
		}
		/* get token */
		encode = 0;
		if (*p == '"')
			quote = *p++;
		else
			quote = 0;
		for (t = p; *p; p++) {
			if (quote) {
				if (*p == quote)
					break;
			} else {
				if (isspace(*p) || *p == '"')
					break;
			}
			if (*p & 0x80)
				encode = 1;
		}
		if (!encode) {
			/*
			 * cannot split token
			 * insert "\n " if there is no room
			 */
			if (quote) {
				if (*p == quote)
					p++;
				t--;
			}
			if (width >= fold/2 && width + (p - s) > fold) {
				putc('\n', fp);
				putc(' ', fp);
				width = 1;
				s = t;		/* skip leading spaces */
			}
			fwrite(s, p - s, 1, fp);
			width += p - s;
		} else {
			/* open quote is counted as slen */
			slen = t - s;
			if (prevenc && slen > 0) {
				/* encode space to keep it */
				slen = 0;
				t = s;
			}
			width += slen;
			while (t < p) {
				if (encode_hdr(&t, p, &cbuf, fold - width)) {
					if (slen) {
						/* OK put leading spaces */
						fwrite(s, slen, 1, fp);
						slen = 0;
					}
					fwrite(cbuf.ptr, cbuf.len, 1, fp);
					width += cbuf.len;
				} else {
					if (fold == CHARBLOCK) {
						/* give up */
						break;
					} else if (width == 1) {
						/* cannot fold */
						fold = CHARBLOCK;
					} else {
						putc('\n', fp);
						putc(' ', fp);
						width = 1;
						if (slen > 0) {
							slen--;
							s++;
						}
					}
				}
			}
			if (quote) {
				putc(quote, fp);
				width++;
				if (*p == quote)
					p++;
			}
		}
		prevenc = encode;
		if (*p == '\n') {
			putc(*p, fp);
			p++;
			width = 0;
		}
	}
}

#define	ENC(c1,c2,c3)	(((c1) << 16) | ((c2) << 8) | (c3))
#define	EOUT(fp, c, n)	putc(b64[((c) >> (6 * (3-n))) & 0x3f], (fp))

void
save_base64_encode(FILE *fp, cbuf_t *buf)
{
	int w;
	unsigned long c;
	u_char *p, *ep;

	for (p = (u_char *)buf->ptr, ep = p + buf->len, w = 0; p + 2 < ep; p += 3) {
		c = ENC(p[0], p[1], p[2]);
		EOUT(fp, c, 0); EOUT(fp, c, 1); EOUT(fp, c, 2); EOUT(fp, c, 3);
		if ((w += 4) >= 72) {
			putc('\n', fp);
			w = 0;
		}
	}
	switch (ep - p) {
	case 1:
		c = ENC(p[0], 0, 0);
		EOUT(fp, c, 0); EOUT(fp, c, 1);
		putc('=', fp); putc('=', fp);
		w += 4;
		break;
	case 2:
		c = ENC(p[0], p[1], 0);
		EOUT(fp, c, 0); EOUT(fp, c, 1); EOUT(fp, c, 2);
		putc('=', fp);
		w += 4;
		break;
	}
	if (w != 0)
		putc('\n', fp);
}

#ifdef TEST_MAIN
main()
{
	char buf[] = "=?iso-2022-jp?B?GyRCTytGLzJKM1g4JjVmPWobKEI=?=";
	char *p;
	cbuf_t res;

	decode_init();
	p = buf;
	if (decode_header(NULL, &p, buf + sizeof(buf)-1, &res))
		printf("%.*s\n", res.len, res.ptr);
	else
		printf("Decode failed\n");
	exit(0);
}
#endif
