/*
 * convert from text form of arbitrary data (e.g., keys) to binary
 * Copyright (C) 2000  Henry Spencer.
 * 
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Library General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/lgpl.txt>.
 * 
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
 * License for more details.
 *
 * $Id$
 */

#include "ttodata.h"

#include <string.h>
#include <ctype.h>

/* converters and misc */
static int unhex(const char *, char *, size_t);
static int unb64(const char *, char *, size_t);
static int untext(const char *, char *, size_t);
static const char *badch(const char *, int, char *, size_t);

/* internal error codes for converters */
#define	SHORT	(-2)		/* internal buffer too short */
#define	BADPAD	(-3)		/* bad base64 padding */
#define	BADCH0	(-4)		/* invalid character 0 */
#define	BADCH1	(-5)		/* invalid character 1 */
#define	BADCH2	(-6)		/* invalid character 2 */
#define	BADCH3	(-7)		/* invalid character 3 */
#define	BADOFF(code) (BADCH0-(code))

/**
 * convert text to data, with verbose error reports
 * 
 * If some of this looks slightly odd, it's because it has changed
 * repeatedly (from the original atodata()) without a major rewrite.
 *
 * @param src
 * @param srclen	0 means apply strlen()
 * @param base 		0 means figure it out
 * @param dst 		need not be valid if dstlen is 0
 * @param dstlen	
 * @param lenp		where to record length (NULL is nowhere)
 * @param errp		error buffer
 * @param flags
 * @return			NULL on success, else literal or errp
 */
const char *ttodatav(const char *src, size_t srclen, int base, char *dst, size_t dstlen, size_t *lenp, char *errp, size_t errlen, unsigned int flags)
{
	size_t ingroup;	/* number of input bytes converted at once */
	char buf[4];		/* output from conversion */
	int nbytes;		/* size of output */
	int (*decode)(const char *, char *, size_t);
	char *stop;
	int ndone;
	int i;
	int underscoreok;
	int skipSpace = 0;

	if (srclen == 0)
	{
		srclen = strlen(src);
	}
	if (dstlen == 0)
	{
		dst = buf;	/* point it somewhere valid */
	}
	stop = dst + dstlen;

	if (base == 0)
	{
		if (srclen < 2)
		{
			return "input too short to be valid";
		}
		if (*src++ != '0')
		{
			return "input does not begin with format prefix";
		}
		switch (*src++)
		{
			case 'x':
			case 'X':
				base = 16;
				break;
			case 's':
			case 'S':
				base = 64;
				break;
			case 't':
			case 'T':
				base = 256;
				break;
			default:
				return "unknown format prefix";
		}
		srclen -= 2;
	}
	switch (base)
	{
		case 16:
			decode = unhex;
			underscoreok = 1;
			ingroup = 2;
			break;
		case 64:
			decode = unb64;
			underscoreok = 0;
			ingroup = 4;
			if(flags & TTODATAV_IGNORESPACE)
			{
				skipSpace = 1;
			}
			break;
		case 256:
			decode = untext;
			ingroup = 1;
			underscoreok = 0;
			break;
		default:
			return "unknown base";
	}

	/* proceed */
	ndone = 0;
	while (srclen > 0)
	{
		char stage[4];	/* staging area for group */
		size_t sl = 0;

		/* Grab ingroup characters into stage,
		 * squeezing out blanks if we are supposed to ignore them.
		 */
		for (sl = 0; sl < ingroup; src++, srclen--)
		{
			if (srclen == 0)
			{
				return "input ends in mid-byte, perhaps truncated";
			}
			else if (!(skipSpace && (*src == ' ' || *src == '\t')))
			{
				stage[sl++] = *src;
			}
		}
		
		nbytes = (*decode)(stage, buf, sizeof(buf));
		switch (nbytes)
		{
			case BADCH0:
			case BADCH1:
			case BADCH2:
			case BADCH3:
				return badch(stage, nbytes, errp, errlen);
			case SHORT:
				return "internal buffer too short (\"can't happen\")";
			case BADPAD:
				return "bad (non-zero) padding at end of base64 input";
		}
		if (nbytes <= 0)
		{
			return "unknown internal error";
		}
		for (i = 0; i < nbytes; i++)
		{
			if (dst < stop)
			{
				*dst++ = buf[i];
			}
			ndone++;
		}
		while (srclen >= 1 && skipSpace && (*src == ' ' || *src == '\t'))
		{
			src++;
			srclen--;
		}
		if (underscoreok && srclen > 1 && (*src == '_' || *src == ':'))
		{
			/* srclen > 1 means not last character */
			src++;
			srclen--;
		}
	}

	if (ndone == 0)
	{
		return "no data bytes specified by input";
	}
	if (lenp != NULL)
	{
		*lenp = ndone;
	}
	return NULL;
}

/**
 * ttodata - convert text to data
 * 
 * @param src
 * @param srclen	0 means apply strlen()
 * @param base		0 means figure it out
 * @param dst		need not be valid if dstlen is 0
 * @param dstlen
 * @param lenp		where to record length (NULL is nowhere)
 * @return			NULL on success, else literal
 */
const char *ttodata(const char *src, size_t srclen, int base, char *dst, size_t dstlen, size_t *lenp)
{
	return ttodatav(src, srclen, base, dst, dstlen, lenp, (char *)NULL,
			(size_t)0, TTODATAV_SPACECOUNTS);
}

/**
 * atodata - convert ASCII to data
 * 
 * backward-compatibility interface
 * 
 * @param src
 * @param srclen
 * @param dst
 * @param dstlen
 * @return 			0 for failure, true length for success
 */
size_t atodata(const char *src, size_t srclen, char *dst, size_t dstlen)
{
	size_t len;
	const char *err;

	err = ttodata(src, srclen, 0, dst, dstlen, &len);
	return (err)? 0:len;
}

/**
 *  atobytes - convert ASCII to data bytes
 *
 * another backward-compatibility interface
 */
const char *atobytes(const char *src, size_t srclen, char *dst, size_t dstlen, size_t *lenp)
{
	return ttodata(src, srclen, 0, dst, dstlen, lenp);
}

/**
 * unhex - convert two ASCII hex digits to byte
 * 
 * @param src 		known to be full length
 * @param dstnumber of result bytes, or error code
 * @param dstlen	not large enough is a failure
 * @return			
 */
static int unhex(const char *src, char *dst, size_t dstlen)
{
	char *p;
	unsigned byte;
	static char hex[] = "0123456789abcdef";

	if (dstlen < 1)
	{
		return SHORT;
	}

	p = strchr(hex, *src);
	if (p == NULL)
	{
		p = strchr(hex, tolower(*src));
	}
	if (p == NULL)
	{
		return BADCH0;
	}
	byte = (p - hex) << 4;
	src++;

	p = strchr(hex, *src);
	if (p == NULL)
	{
		p = strchr(hex, tolower(*src));
	}
	if (p == NULL)
	{
		return BADCH1;
	}
	byte |= (p - hex);

	*dst = byte;
	return 1;
}

/**
 * unb64 - convert four ASCII base64 digits to three bytes
 *
 * Note that a base64 digit group is padded out with '=' if it represents
 * less than three bytes:  one byte is dd==, two is ddd=, three is dddd.
 *
 * @param src		known to be full length 
 * @param dst		
 * @param dstlen	
 * @return			number of result bytes, or error code
 */
static int unb64(const char *src, char *dst, size_t dstlen)
{
	char *p;
	unsigned byte1;
	unsigned byte2;
	static char base64[] =
	"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

	if (dstlen < 3)
	{
		return SHORT;
	}
	p = strchr(base64, *src++);

	if (p == NULL)
	{
		return BADCH0;
	}
	byte1 = (p - base64) << 2;	/* first six bits */

	p = strchr(base64, *src++);
	if (p == NULL)
	{
		return BADCH1;
	}

	byte2 = p - base64;		/* next six:  two plus four */
	*dst++ = byte1 | (byte2 >> 4);
	byte1 = (byte2 & 0xf) << 4;

	p = strchr(base64, *src++);
	if (p == NULL)
	{
		if (*(src-1) == '=' && *src == '=')
		{
			if (byte1 != 0)		/* bad padding */
			{
				return BADPAD;
			}
			return 1;
		}
		return BADCH2;
	}

	byte2 = p - base64;		/* next six:  four plus two */
	*dst++ = byte1 | (byte2 >> 2);
	byte1 = (byte2 & 0x3) << 6;

	p = strchr(base64, *src++);
	if (p == NULL)
	{
		if (*(src-1) == '=')
		{
			if (byte1 != 0)		/* bad padding */
			{
				return BADPAD;
			}
			return 2;
		}
		return BADCH3;
	}
	byte2 = p - base64;		/* last six */
	*dst++ = byte1 | byte2;

	return 3;
}

/**
 * untext - convert one ASCII character to byte
 * 
 * @param src		known to be full length
 * @param dst		
 * @param dstlen	not large enough is a failure
 * @return 			number of result bytes, or error code
 */
static int untext(const char *src, char *dst, size_t dstlen)
{
	if (dstlen < 1)
	{
		return SHORT;
	}
	*dst = *src;
	return 1;
}

/**
 * badch - produce a nice complaint about an unknown character
 *
 * If the compiler complains that the array bigenough[] has a negative
 * size, that means the TTODATAV_BUF constant has been set too small.
 * 
 * @param src		
 * @param errcode	
 * @param errp		might be NULL
 * @param errlen	
 * @return			literal or errp
 */
static const char *badch(const char *src, int errcode, char *errp, size_t errlen)
{
	static const char pre[] = "unknown character (`";
	static const char suf[] = "') in input";
	char buf[5];
#	define	REQD	(sizeof(pre) - 1 + sizeof(buf) - 1 + sizeof(suf))
	struct sizecheck {
		char bigenough[TTODATAV_BUF - REQD];	/* see above */
	};
	char ch;

	if (errp == NULL || errlen < REQD)
	{
		return "unknown character in input";
	}
	strcpy(errp, pre);
	ch = *(src + BADOFF(errcode));
	if (isprint(ch))
	{
		buf[0] = ch;
		buf[1] = '\0';
	}
	else
	{
		buf[0] = '\\';
		buf[1] = ((ch & 0700) >> 6) + '0';
		buf[2] = ((ch & 0070) >> 3) + '0';
		buf[3] = ((ch & 0007) >> 0) + '0';
		buf[4] = '\0';
	}
	strcat(errp, buf);
	strcat(errp, suf);
	return (const char *)errp;
}
