/***********************************************************************
*                                                                      *
*               This software is part of the ast package               *
*          Copyright (c) 1985-2011 AT&T Intellectual Property          *
*          Copyright (c) 2020-2024 Contributors to ksh 93u+m           *
*                      and is licensed under the                       *
*                 Eclipse Public License, Version 2.0                  *
*                                                                      *
*                A copy of the License is available at                 *
*      https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.html      *
*         (with md5 checksum 84283fa8859daf213bdda5a9f8d1be1d)         *
*                                                                      *
*                 Glenn Fowler <gsf@research.att.com>                  *
*                  David Korn <dgk@research.att.com>                   *
*                   Phong Vo <kpv@research.att.com>                    *
*                  Martijn Dekker <martijn@inlv.org>                   *
*                                                                      *
***********************************************************************/

/*
 * Glenn Fowler
 * AT&T Research
 *
 * mime/mailcap support library
 */

static const char lib[] = "libast:mime";

#include "mimelib.h"

typedef struct Att_s
{
	struct Att_s*	next;
	char*		name;
	char*		value;
} Att_t;

typedef struct Cap_s
{
	struct Cap_s*	next;
	unsigned long	flags;
	Att_t		att;
	char*		test;
	char		data[1];
} Cap_t;

typedef struct
{
	Dtlink_t	link;
	Cap_t*		cap;
	Cap_t*		pac;
	char		name[1];
} Ent_t;

typedef struct
{
	char*		data;
	int		size;
} String_t;

typedef struct
{
	char*		next;
	String_t	name;
	String_t	value;
} Parse_t;

typedef struct
{
	const char*	pattern;
	int		prefix;
	Sfio_t*		fp;
	int		hit;
} Walk_t;

/*
 * convert c to lower case
 */

static int
lower(int c)
{
	return isupper(c) ? tolower(c) : c;
}

/*
 * Ent_t case insensitive comparf
 */

static int
order(Dt_t* dt, void* a, void* b, Dtdisc_t* disc)
{
	return strcasecmp(a, b);
}

/*
 * Cap_t free
 */

static void
dropcap(Cap_t* cap)
{
	Att_t*	att;

	while (att = cap->att.next)
	{
		cap->att.next = att->next;
		free(att);
	}
	free(cap);
}

/*
 * Ent_t freef
 */

static void
drop(Dt_t* dt, void* object, Dtdisc_t* disc)
{
	Ent_t*	ent = (Ent_t*)object;
	Cap_t*	cap;

	while (cap = ent->cap)
	{
		ent->cap = cap->next;
		dropcap(cap);
	}
	free(ent);
}

/*
 * add mime type entry in s to mp
 */

int
mimeset(Mime_t* mp, char* s, unsigned long flags)
{
	Ent_t*	ent;
	Cap_t*	cap;
	Att_t*	att;
	char*	t;
	char*	v;
	char*	k = NULL;
	char*	x = NULL;
	Att_t*	tta = NULL;
	int	q;

	for (; isspace(*s); s++);
	if (*s && *s != '#')
	{
		cap = 0;
		for (v = s; *v && *v != ';'; v++)
			if (isspace(*v) || *v == '/' && *(v + 1) == '*')
				*v = 0;
		if (*v)
		{
			*v++ = 0;
			do
			{
				for (; isspace(*v); v++);
				if (cap)
				{
					for (t = v; *t && !isspace(*t) && *t != '='; t++);
					for (k = t; isspace(*t); t++);
					if (!*t || *t == '=' || *t == ';')
					{
						if (*t)
							while (isspace(*++t));
						*k = 0;
						k = v;
						v = t;
					}
					else
						k = 0;
				}
				if (*v == '"')
					q = *v++;
				else
					q = 0;
				for (t = v; *t; t++)
					if (*t == '\\')
					{
						switch (*(t + 1))
						{
						case 0:
						case '\\':
						case '%':
							*t = *(t + 1);
							break;
						default:
							*t = ' ';
							break;
						}
						if (!*++t)
							break;
					}
					else if (*t == q)
					{
						*t = ' ';
						q = 0;
					}
					else if (*t == ';' && !q)
					{
						*t = ' ';
						break;
					}
				for (; t > v && isspace(*(t - 1)); t--);
				if (t <= v && (!cap || !k))
					break;
				if (!cap)
				{
					if (!(cap = newof(0, Cap_t, 1, strlen(v) + 1)))
						return -1;
					if (*t)
						*t++ = 0;
					tta = &cap->att;
					tta->name = "default";
					x = strcopy(tta->value = cap->data, v) + 1;
				}
				else if (k)
				{
					if (*t)
						*t++ = 0;
					if (!(att = newof(0, Att_t, 1, 0)))
						return -1;
					if (!x)
						abort();
					x = strcopy(att->name = x, k) + 1;
					x = strcopy(att->value = x, v) + 1;
					tta->next = att;
					tta = att;
					if (!strcasecmp(k, "test"))
						cap->test = att->value;
				}
			} while (*(v = t));
		}
		ent = (Ent_t*)dtmatch(mp->cap, s);
		if (cap)
		{
			if (ent)
			{
				Cap_t*	dup;
				Cap_t*	pud;

				for (pud = 0, dup = ent->cap; dup; pud = dup, dup = dup->next)
					if (!cap->test && !dup->test || cap->test && dup->test && streq(cap->test, dup->test))
					{
						if (flags & MIME_REPLACE)
						{
							if (pud)
								pud->next = cap;
							else
								ent->cap = cap;
							if (!(cap->next = dup->next))
								ent->pac = cap;
							cap = dup;
						}
						dropcap(cap);
						return 0;
					}
				ent->pac = ent->pac->next = cap;
			}
			else if (!(ent = newof(0, Ent_t, 1, strlen(s) + 1)))
				return -1;
			else
			{
				strcpy(ent->name, s);
				ent->cap = ent->pac = cap;
				dtinsert(mp->cap, ent);
			}
		}
		else if (ent && (flags & MIME_REPLACE))
			dtdelete(mp->cap, ent);
	}
	return 0;
}

/*
 * load mime type files into mp
 */

int
mimeload(Mime_t* mp, const char* file, unsigned long flags)
{
	char*	s;
	char*	t;
	char*	e;
	int	n;
	Sfio_t*	fp;

	if (!(s = (char*)file))
	{
		flags |= MIME_LIST;
		if (!(s = getenv(MIME_FILES_ENV)))
			s = MIME_FILES;
	}
	for (;;)
	{
		if (!(flags & MIME_LIST))
			e = 0;
		else if (e = strchr(s, ':'))
		{
			/*
			 * ok, so ~ won't work for the last list element
			 * we do it for MIME_FILES_ENV anyway
			 */

			if ((strneq(s, "~/", n = 2) || strneq(s, "$HOME/", n = 6) || strneq(s, "${HOME}/", n = 8)) && (t = getenv("HOME")))
			{
				sfputr(mp->buf, t, -1);
				s += n - 1;
			}
			sfwrite(mp->buf, s, e - s);
			if (!(s = sfstruse(mp->buf)))
				return -1;
		}
		if (fp = tokline(s, SFIO_READ, NULL))
		{
			while (t = sfgetr(fp, '\n', 1))
				if (mimeset(mp, t, flags))
					break;
			sfclose(fp);
		}
		else if (!(flags & MIME_LIST))
			return -1;
		if (!e)
			break;
		s = e + 1;
	}
	return 0;
}

/*
 * mimelist walker
 */

static int
list(Dt_t* dt, void* object, void* context)
{
	Walk_t*	wp = (Walk_t*)context;
	Ent_t*		ent = (Ent_t*)object;
	Cap_t*		cap;
	Att_t*		att;

	if (!wp->pattern || !strncasecmp(ent->name, wp->pattern, wp->prefix) && (!ent->name[wp->prefix] || ent->name[wp->prefix] == '/'))
	{
		wp->hit++;
		for (cap = ent->cap; cap; cap = cap->next)
		{
			sfprintf(wp->fp, "%s", ent->name);
			for (att = &cap->att; att; att = att->next)
			{
				sfprintf(wp->fp, "\n\t");
				if (att != &cap->att)
				{
					sfprintf(wp->fp, "%s", att->name);
					if (*att->value)
						sfprintf(wp->fp, " = ");
				}
				sfputr(wp->fp, att->value, -1);
			}
			sfprintf(wp->fp, "\n");
		}
	}
	return 0;
}

/*
 * find entry matching type
 * if exact match fails then left and right x- and right version number
 * permutations are attempted
 */

static Ent_t*
find(Mime_t* mp, const char* type)
{
	char*	lp;
	char*	rp;
	char*	rb;
	char*	rv;
	int	rc;
	int	i;
	char*	s;
	Ent_t*	ent;
	char	buf[256];

	static const char*	prefix[] = { "", "", "x-", "x-", "" };

	if ((ent = (Ent_t*)dtmatch(mp->cap, type)) ||
	    !(rp = strchr(lp = (char*)type, '/')) ||
	    strlen(lp) >= sizeof(buf))
		return ent;
	strcpy(buf, type);
	rp = buf + (rp - lp);
	*rp++ = 0;
	if (*rp == 'x' && *(rp + 1) == '-')
		rp += 2;
	lp = buf;
	if (*lp == 'x' && *(lp + 1) == '-')
		lp += 2;
	rb = rp;
	for (rv = rp + strlen(rp); rv > rp && (isdigit(*(rv - 1)) || *(rv - 1) == '.'); rv--);
	rc = *rv;
	do
	{
		rp = rb;
		do
		{
			for (i = 0; i < elementsof(prefix) - 1; i++)
			{
				sfprintf(mp->buf, "%s%s/%s%s", prefix[i], lp, prefix[i + 1], rp);
				if (!(s = sfstruse(mp->buf)))
					return 0;
				if (ent = (Ent_t*)dtmatch(mp->cap, s))
					return ent;
				if (rc)
				{
					*rv = 0;
					sfprintf(mp->buf, "%s%s/%s%s", prefix[i], lp, prefix[i + 1], rp);
					if (!(s = sfstruse(mp->buf)))
						return 0;
					if (ent = (Ent_t*)dtmatch(mp->cap, s))
						return ent;
					*rv = rc;
				}
			}
			while (*rp && *rp++ != '-');
		} while (*rp);
		while (*lp && *lp++ != '-');
	} while (*lp);
	return (Ent_t*)dtmatch(mp->cap, buf);
}

/*
 * list mime <type,data> for pat on fp
 */

int
mimelist(Mime_t* mp, Sfio_t* fp, const char* pattern)
{
	Ent_t*	ent;
	Walk_t	ws;

	ws.fp = fp;
	ws.hit = 0;
	ws.prefix = 0;
	if (ws.pattern = pattern)
	{
		while (*pattern && *pattern++ != '/');
		if (!*pattern || *pattern == '*' && !*(pattern + 1))
			ws.prefix = pattern - ws.pattern;
		else if (ent = find(mp, ws.pattern))
		{
			ws.pattern = 0;
			list(mp->cap, ent, &ws);
			return ws.hit;
		}
	}
	dtwalk(mp->cap, list, &ws);
	return ws.hit;
}

/*
 * get next arg in pp
 * 0 returned if no more args
 */

static int
arg(Parse_t* pp, int first)
{
	char*	s;
	int	c;
	int	q;
	int	x;

	for (s = pp->next; isspace(*s) && *s != '\n'; s++);
	if (!*s || *s == '\n')
	{
		pp->next = s;
		return 0;
	}
	pp->name.data = s;
	pp->value.data = 0;
	q = 0;
	x = 0;
	while ((c = *s++) && c != ';' && c != '\n')
	{
		if (c == '"')
		{
			q = 1;
			if (pp->value.data)
			{
				pp->value.data = s;
				if (x)
					x = -1;
				else
					x = 1;
			}
			else if (!x && pp->name.data == (s - 1))
			{
				x = 1;
				pp->name.data = s;
			}
			do
			{
				if (!(c = *s++) || c == '\n')
				{
					s--;
					break;
				}
			} while (c != '"');
			if (first < 0 || x > 0)
			{
				c = ';';
				break;
			}
 		}
		else if (c == '=' && !first)
		{
			first = 1;
			pp->name.size = s - pp->name.data - 1;
			pp->value.data = s;
		}
		else if (first >= 0 && isspace(c))
			break;
	}
	pp->next = s - (c != ';');
	if (first >= 0 || !q)
		for (s--; s > pp->name.data && isspace(*(s - 1)); s--);
	if (pp->value.data)
		pp->value.size = s - pp->value.data - (q && first < 0);
	else
	{
		pp->value.size = 0;
		pp->name.size = s - pp->name.data - (q && first < 0);
	}
	if (first >= 0 && pp->name.size > 0 && pp->name.data[pp->name.size - 1] == ':')
		return 0;
	return pp->name.size > 0;
}

/*
 * low level for mimeview()
 */

static char*
expand(Mime_t* mp, char* s, const char* name, const char* type, const char* opts)
{
	char*	t;
	char*	v;
	int	c;
	int	e;
	int	n;
	Parse_t	pp;

	mp->disc->flags |= MIME_PIPE;
	for (;;)
	{
		switch (c = *s++)
		{
		case 0:
		case '\n':
			break;
		case '%':
			if ((c = *s++) == '{' && (e = '}') || c == '(' && (e = ')'))
			{
				for (t = s; *s && *s != e; s++);
				n = s - t;
				switch (*s)
				{
				case '}':
					s++;
					c = '{';
					break;
				case ')':
					s++;
					if (c = *s)
						s++;
					break;
				}
			}
			else
				t = 0;
			switch (c)
			{
			case 's':
				v = (char*)name;
				mp->disc->flags &= ~MIME_PIPE;
				break;
			case 't':
				v = (char*)type;
				break;
			case '{':
				for (t = s; *s && *s != '}'; s++);
				if (*s && (c = s++ - t) && (pp.next = (char*)opts))
					while (arg(&pp, 0))
						if (pp.name.size == c && !strncasecmp(pp.name.data, t, c))
						{
							if (pp.value.size)
								sfwrite(mp->buf, pp.value.data, pp.value.size);
							break;
						}
				continue;
			default:
				sfputc(mp->buf, c);
				continue;
			}
			if (v && *v)
				n = strlen(v);
			else if (t)
				v = t;
			else
				continue;
			sfputr(mp->buf, fmtquote(v, 0, 0, n, FMT_SHELL), -1);
			continue;
		default:
			sfputc(mp->buf, c);
			continue;
		}
		break;
	}
	return sfstruse(mp->buf);
}

/*
 * return expanded command/path/value for <view,name,type,opts>
 * return value valid until next mime*() call
 */

char*
mimeview(Mime_t* mp, const char* view, const char* name, const char* type, const char* opts)
{
	Ent_t*	ent;
	Cap_t*	cap;
	Att_t*	att;
	char*	s;
	int	c;

	if (ent = find(mp, type))
	{
		cap = ent->cap;
		if (!view || strcasecmp(view, "test"))
			while (s = cap->test)
			{
				if (s = expand(mp, s, name, type, opts))
				{
					Parse_t	a1;
					Parse_t	a2;
					Parse_t	a3;
					Parse_t	a4;

					/*
					 * try to do a few common cases here
					 * mailcap consistency is a winning
					 * strategy
					 */

					a1.next = s;
					if (arg(&a1, -1))
					{
						if ((c = *a1.name.data == '!') && --a1.name.size <= 0 && !arg(&a1, -1))
							goto lose;
						if (a1.name.size == 6 && strneq(a1.name.data, "strcmp", 6) || a1.name.size == 10 && strneq(a1.name.data, "strcasecmp", 10))
						{
							a2.next = a1.next;
							if (!arg(&a2, -1))
								goto lose;
							a3.next = a2.next;
							if (!arg(&a3, -1))
								goto lose;
							if (a2.name.size != a3.name.size)
								c ^= 0;
							else c ^= (a1.name.size == 6 ? strncmp : strncasecmp)(a2.name.data, a3.name.data, a2.name.size) == 0;
							if (c)
								break;
							goto skip;
						}
						else if (a1.name.size == 4 && strneq(a1.name.data, "test", 4))
						{
							if (!arg(&a1, -1))
								goto lose;
							a2.next = a1.next;
							if (!arg(&a2, -1) || a2.name.size > 2 || a2.name.size == 1 && *a2.name.data != '=' || a2.name.size == 2 && (!strneq(a1.name.data, "!=", 2) || !strneq(a2.name.data, "==", 2)))
								goto lose;
							a3.next = a2.next;
							if (!arg(&a3, -1))
								goto lose;
							if (*a3.name.data == '`' && *(a3.name.data + a3.name.size - 1) == '`')
							{
								a4 = a3;
								a3 = a1;
								a1 = a4;
							}
							if (*a1.name.data == '`' && *(a1.name.data + a1.name.size - 1) == '`')
							{
								a1.next = a1.name.data + 1;
								if (!arg(&a1, -1) || a1.name.size != 4 || !strneq(a1.name.data, "echo", 4) || !arg(&a1, -1))
									goto lose;
								a4.next = a1.next;
								if (!arg(&a4, 1) || a4.name.size < 21 || !strneq(a4.name.data, "| tr '[A-Z]' '[a-z]'`", 21))
									goto lose;
							}
							else
								a4.name.size = 0;
							c = *a2.name.data == '!';
							if (a1.name.size != a3.name.size)
								c ^= 0;
							else c ^= (a4.name.size ? strncasecmp : strncmp)(a1.name.data, a3.name.data, a1.name.size) == 0;
							if (c)
								break;
							goto skip;
						}
					}
				lose:
					if (!system(s))
						break;
				}
			skip:
				if (!(cap = cap->next))
					return NULL;
			}
		att = &cap->att;
		if (view && *view && !streq(view, "-"))
			while (strcasecmp(view, att->name))
				if (!(att = att->next))
					return NULL;
		return expand(mp, att->value, name, type, opts);
	}
	return NULL;
}

/*
 * lower case identifier prefix strcmp
 * if e!=0 then it will point to the next char after the match
 */

int
mimecmp(const char* s, const char* v, char** e)
{
	int	n;

	while (isalnum(*v) || *v == *s && (*v == '_' || *v == '-' || *v == '/'))
		if (n = lower(*s++) - lower(*v++))
			return n;
	if (!isalnum(*s) && *s != '_' && *s != '-')
	{
		if (e)
			*e = (char*)s;
		return 0;
	}
	return lower(*s) - lower(*v);
}

/*
 * parse mime headers in strsearch(tab,num,siz) from s
 * return >0 if mime header consumed
 */

int
mimehead(Mime_t* mp, void* tab, size_t num, size_t siz, char* s)
{
	void*	p;
	char*		e;
	Parse_t		pp;
	Mimevalue_f	set;

	set = mp->disc->valuef;
	if (!strncasecmp(s, "original-", 9))
		s += 9;
	if (!strncasecmp(s, "content-", 8))
	{
		s += 8;
		if ((p = strsearch(tab, num, siz, (Strcmp_f)mimecmp, s, &e)) && *e == ':')
		{
			pp.next = e + 1;
			if (arg(&pp, 1))
			{
				if ((*set)(mp, p, pp.name.data, pp.name.size, mp->disc))
					return 0;
				while (arg(&pp, 0))
					if (pp.value.size &&
					    (p = strsearch(tab, num, siz, (Strcmp_f)mimecmp, pp.name.data, &e)) &&
					    (*set)(mp, p, pp.value.data, pp.value.size, mp->disc))
						return 0;
				return 1;
			}
		}
		else if (strchr(s, ':'))
			return 1;
	}
	return !strncasecmp(s, "x-", 2);
}

/*
 * open a mime library handle
 */

Mime_t*
mimeopen(Mimedisc_t* disc)
{
	Mime_t*	mp;

	if (!(mp = newof(0, Mime_t, 1, 0)))
		return NULL;
	mp->id = lib;
	mp->disc = disc;
	mp->dict.key = offsetof(Ent_t, name);
	mp->dict.comparf = order;
	mp->dict.freef = drop;
	if (!(mp->buf = sfstropen()) || !(mp->cap = dtopen(&mp->dict, Dtoset)))
	{
		mimeclose(mp);
		return NULL;
	}
	return mp;
}

/*
 * close a mimeopen() handle
 */

int
mimeclose(Mime_t* mp)
{
	if (mp)
	{
		if (mp->buf)
			sfclose(mp->buf);
		if (mp->cap)
			dtclose(mp->cap);
		if (mp->freef)
			(*mp->freef)(mp);
		free(mp);
	}
	return 0;
}
