/*
 * unmunge.c -- Program to convert a munged file to original form
 *
 * Copyright (C) 1997 Pretty Good Privacy, Inc.
 *
 * Designed by Colin Plumb, Mark H. Weaver, and Philip R. Zimmermann
 * Written by Mark H. Weaver
 *
 * $Id: unmunge.c,v 1.4 1997/07/09 15:07:51 colin Exp $
 */

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

#include "crc.h"
#include "util.h"

typedef struct UnMungeState
{
	char const *	mungedName;
	char			fileName[128];
	char *			fileNameTail;
	int				binaryMode, tabWidth;
	long			productNumber, fileNumber, pageNumber, lineNumber;
	word16			hdrFlags;
	word32			runningCRC, pageCRC;
	FILE *			file;
	FILE *			out;
} UnMungeState;

/* Inverse table from characters to original 6-bit value */
byte	radix64Inv[256];


static void
InitRadix64InvTable(void)
{
	int		i;

	for (i = 0; i < 256; i++)
		radix64Inv[i] = -1;
	for (i = 0; i < 64; i++)
		radix64Inv[(byte)radix64Digits[i]] = i;
}

/* Returns number of characters decoded, or -1 on error */
static int
Decode4(char const src[4], byte dest[3])
{
	int		i, length;
	byte	srcVal[4];

	for (i = 0; i < 4 && src[i] != '='; i++)
		if ((srcVal[i] = radix64Inv[(byte)src[i]]) == (byte) -1)
			return 1;

	length = i - 1;
	if (length < 1)
		return -1;

	for (; i < 4; i++)
		srcVal[0] = 0;

	dest[0] = (srcVal[0] << 2) | (srcVal[1] >> 4);
	dest[1] = (srcVal[1] << 4) | (srcVal[2] >> 2);
	dest[2] = (srcVal[2] << 6) | (srcVal[3]);

	return length;
}

/*
 * Return number of characters decoded, or -1 on error
 */
static int
DecodeLine(char const *src, char *dest, int srclength)
{
	int destlength = 0;
	int result;

	if (srclength % 4 || !srclength)
		return -1;	/* Must be a multiple of 4 */
	
	while (srclength -= 4) {
		if (Decode4(src, dest + destlength) != 3)
			return -1;
		src += 4;
		destlength += 3;
	}
	result = Decode4(src, dest + destlength);
	if (result < 1)
		return -1;
	return destlength + result;
}

int PrintFileError(UnMungeState *state, char const *message)
{
	fprintf(stderr, "%s, %s line %ld\n", message,
			state->mungedName, state->lineNumber);
	return 1;
}

int UnMungeFile(char const *mungedName, int forceOverwrite)
{
	UnMungeState *	state;
	char			buffer[512];
	char			outbuf[BYTES_PER_LINE+1];
	char *			line;
	char *			lineData;
	char *			p;
	int				length;
	int				result = 0;
	word16			crc16;

	state = (UnMungeState *)calloc(1, sizeof(*state));
	state->mungedName = mungedName;

	state->file = fopen(state->mungedName, "r");
	if (state->file == NULL)
	{
		result = errno;
		goto error;
	}

	while (!feof(state->file))
	{
		if (fgets(buffer, sizeof(buffer), state->file) == NULL)
		{
			if (feof(state->file))
				break;
			goto fileError;
		}

		state->lineNumber++;

		line = buffer;
		/* Strip leading whitespace */
		while (isspace(*line))
			line++;
		if (*line == '\0')
			continue;

		/* Strip trailing whitespace */
		p = line + strlen(line);
		while (p > line && (byte)p[-1] < 128 && isspace(p[-1]))
			p--;

		lineData = line + PREFIX_LENGTH;

		/* Pad up to at least PREFIX_LENGTH */
		while (p < lineData)
			*p++ = ' ';
		*p++ = '\n';
		*p = '\0';
		length = p - lineData;

		crc16 = CalculateCRC16(0, (byte const *)lineData, length);

		p = line + 2;
		if (crc16 != strtol(p, &p, 16) || p != line + 6)
		{
			result = PrintFileError(state, "ERROR: CRC16 failed");
			goto error;
		}

		if (line[0] == '-')
		{
			int				formatVersion;
			int				flags;
			unsigned long	pageCRC;
			int				tabWidth;
			long			productNumber;
			long			fileNumber;
			long			pageNumber;
			char *			fileNameTail;

			/* Header line */
			if (line[1] != '-')
			{
			invalidHeader:
				result = PrintFileError(state, "ERROR: Invalid header");
				goto error;
			}

			if (sscanf(lineData, "%1x%2x%8lx%1x%3lx%4lx Page %ld of ",
					   &formatVersion, &flags, &pageCRC, &tabWidth,
					   &productNumber, &fileNumber, &pageNumber) < 7)
				goto invalidHeader;

			if (formatVersion > 0)
			{
				result = PrintFileError(state,
										"ERROR: Format too new for "
											"this version of unmunge");
				goto error;
			}

			fileNameTail = strstr(line, " of ");
			if (fileNameTail == NULL)
				goto invalidHeader;
			fileNameTail += 4;
			p = fileNameTail + strlen(fileNameTail);
			if (p < fileNameTail + 3 || p[-1] != '\n')
				goto invalidHeader;
			else
				p[-1] = '\0';

			if (state->out != NULL && state->runningCRC != state->pageCRC)
			{
				result = PrintFileError(state,
								"ERROR: CRC32 mismatch on page before");
				goto error;
			}

			if ((state->hdrFlags & HDR_FLAG_LASTPAGE) && state->out != NULL)
			{
				fclose(state->out);
				state->out = NULL;
			}

			if (state->out != NULL)
			{
				if (pageNumber != state->pageNumber + 1 ||
						fileNumber != state->fileNumber ||
						productNumber != state->productNumber ||
						tabWidth != state->tabWidth ||
						strcmp(fileNameTail, state->fileNameTail) != 0)
				{
					result = PrintFileError(state,
									"ERROR: Missing pages of previous file");
					goto error;
				}
			}
			else
			{
				if (pageNumber != 1)
					goto invalidHeader;

				/* TODO: Use global filelist to get pathname */
				strcpy(state->fileName, fileNameTail);
				state->fileNameTail = state->fileName;
				state->binaryMode = (tabWidth == 0);

				if (!forceOverwrite)
				{
					FILE *	file;

					/* Make sure file doesn't already exist */
					file = fopen(state->fileName, "r");
					if (file != NULL)
					{
						fclose(file);
						fprintf(stderr, "ERROR: %s already exists\n",
								state->fileName);
						result = 1;
						goto error;
					}
				}

				state->out = fopen(state->fileName,
								   state->binaryMode ? "wb" : "w");
				if (state->out == NULL)
				{
					result = errno;
					goto error;
				}
			}

			state->runningCRC = 0;
			state->pageCRC = (word32)pageCRC;
			state->hdrFlags = (word16)flags;
			state->pageNumber = pageNumber;
			state->fileNumber = fileNumber;
			state->productNumber = productNumber;
			state->tabWidth = tabWidth;
		}
		else
		{
			if (state->out == NULL)
			{
				result = PrintFileError(state, "ERROR: Missing header line");
				goto error;
			}

			/* Normal data line */
			state->runningCRC = CalculateCRC32(state->runningCRC,
											   (byte const *)lineData,
											   length);
			line[2] = '\0';
			p = line;
			if ((long)(state->runningCRC >> 24) != strtol(p, &p, 16)
					|| p != line + 2)
			{
				result = PrintFileError(state, "ERROR: CRC32 failed");
				goto error;
			}

			if (state->binaryMode)
			{
				length = DecodeLine(lineData, outbuf, length-1);
				if (length < 0 || length > BYTES_PER_LINE) {
					result = PrintFileError(state,
									"ERROR: Corrupt radix-64 data");
					goto error;
				}
				fwrite(outbuf, 1, length, state->out);
			}
			else
			{
				p = lineData;
				while (*p != '\0')
				{
					if (*p == TAB_CHAR)
					{
						p++;
						putc('\t', state->out);
						while ((p - lineData) % state->tabWidth)
						{
							if (*p == '\n')
								break;
							else if (*p == ' ')
								p++;
							else
							{
								result = PrintFileError(state,
												"ERROR: Not enough spaces "
												"after a tab character");
								goto error;
							}
						}
					}
					else if (*p == FORMFEED_CHAR)
					{
						p++;
						if (*p != '\n')
						{
							result = PrintFileError(state,
											"ERROR: Formfeed character "
											"not at end of line");
							goto error;
						}
						p++;
						putc('\f', state->out);
					}
					else
					{
						putc(*p, state->out);
						p++;
					}
				}
			}
		}
	}
	if (state->out != NULL)
	{
		if (!(state->hdrFlags & HDR_FLAG_LASTPAGE))
		{
			result = PrintFileError(state, "ERROR: Missing pages");
			goto error;
		}
		if (state->runningCRC != state->pageCRC)
		{
			result = PrintFileError(state,
							"ERROR: CRC32 mismatch on page before");
			goto error;
		}
	}

	result = 0;
	goto done;

fileError:
	result = ferror(state->file);

error:
done:
	if (state != NULL)
	{
		if (state->out != NULL)
			fclose(state->out);
		if (state->file != NULL)
			fclose(state->file);
		free(state);
	}
	return result;
}

int main(int argc, char *argv[])
{
	int		result = 0;
	int		forceOverwrite = 0;
	int		i, j;

	InitCRC();
	InitRadix64InvTable();

	for (i = 1; i < argc && argv[i][0] == '-'; i++)
	{
		if (0 == strcmp(argv[i], "--"))
		{
			i++;
			break;
		}
		for (j = 1; argv[i][j] != '\0'; j++)
		{
			if (argv[i][j] == 'f')
			{
				forceOverwrite = 1;
			}
			else
			{
				fprintf(stderr, "ERROR: Unrecognized option -%c\n", argv[i][j]);
				exit(1);
			}
		}
	}

	for (; i < argc; i++)
	{
		if ((result = UnMungeFile(argv[i], forceOverwrite)) != 0)
		{
			/* If result > 0, message should have already been printed */
			if (result < 0)
				fprintf(stderr, "ERROR: %s\n", strerror(result));
			exit(1);
		}
	}
	
	return 0;
}

/*
 * Local Variables:
 * tab-width: 4
 * End:
 * vi: ts=4 sw=4
 * vim: si
 */

