/*
 * Original:
 * Copyright (c) 1999 by Aaron R. Crane.
 * Permission is granted to use, distribute, or modify this source,
 * provided that this copyright notice remains intact.
 *
 * Modified:
 * Copyright (c) 2002 by David I. Bell
 * Permission is granted to use, distribute, or modify this source,
 * provided that this copyright notice remains intact.
 *
 * The "ar" built-in command.
 * This allows extraction and listing of ar files.
 */

#include <ar.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <limits.h>

#include "sash.h"


/*
 * Structure to hold information about the archive file.
 */
typedef struct
{
	int		fd;		/* file reading archive from */
	BOOL		eof;		/* end of file has been seen */
	BOOL		rescan;		/* rescan the header just read */
	unsigned char *	nameTable;	/* long name table */

	/*
	 * Information about the current file read from the archive.
	 * This is extracted from the latest member header read.
	 */
	char *		name;		/* current file name */
	time_t		date;		/* date of file */
	uid_t		uid;		/* user id */
	gid_t		gid;		/* group id */
	mode_t		mode;		/* file protection */
	off_t		size;		/* file size */
	int		pad;		/* padding to next header */
} Archive;


/*
 * Local procedures.
 */
static	void	initArchive(Archive * arch);
static	BOOL	openArchive(const char * name, Archive * arch);
static	void	closeArchive(Archive * arch);
static	BOOL	readSpecialMember(Archive * arch);
static	BOOL	readNormalMember(Archive * arch);
static	BOOL	readMember(Archive * arch, struct ar_hdr * hdr);
static	BOOL	skipMember(const Archive * arch);
static	BOOL	skipPadding(int fd, int pad);
static	BOOL	writeFile(const Archive * arch, int outfd);
static	int	createFile(const Archive * arch);
static	BOOL	canonicalize(Archive * arch, const struct ar_hdr * hdr);
static	void	listMember(const Archive * arch);
static	int	shortNameMatches44BSD(const char * name);
static	int	shortNameMatchesSysV(const char * name);

static	BOOL	wantMember(const Archive * arch, int n_names,
			const char ** names);

static BOOL	getNumber(const char * s, unsigned base, int max_digits,
			unsigned long * ul);


void
do_ar(int argc, const char ** argv)
{
	const char *	options;
	const char *	archiveName;
	BOOL		doExtract;
	BOOL		doTable;
	BOOL		doPrint;
	BOOL		verbose;
	Archive		arch;

	verbose = FALSE;
	doExtract = FALSE;
	doTable = FALSE;
	doPrint = FALSE;

	if (argc < 3)
	{
		fprintf(stderr, "Too few arguments for ar\n");

		return;
	}

	/*
	 * Get the option string and archive file name.
	 */
	options = argv[1];
	archiveName = argv[2];

	/*
	 * Advance the arguments to the list of file names (if any).
	 */
	argc -= 3;
	argv += 3;

	/*
	 * Parse the option characters.
	 */
	for (; *options; options++)
	{
		switch (*options)
		{
		case 't':
			doTable = TRUE;
			break;

		case 'x':
			doExtract = TRUE;
			break;

		case 'p':
			doPrint = TRUE;
			break;

		case 'v':
			verbose = TRUE;
			break;

		case 'd': case 'm': case 'q': case 'r':
			fprintf(stderr, "Writing ar files is not supported\n");

			return;

		default:
			fprintf(stderr, "Unknown ar flag: %c\n", *options);

			return;
		}
	}

	if (doExtract + doTable + doPrint != 1)
	{
		fprintf(stderr,
			"Exactly one of 'x', 'p' or 't' must be specified\n");

		return;
	}

	/*
	 * Open the archive file.
	 */
	initArchive(&arch);

	if (!openArchive(archiveName, &arch))
		return;

	/*
	 * Read the first special member of the archive.
	 */
	if (!readSpecialMember(&arch))
		return;

	/*
	 * Read all of the normal members of the archive.
	 */
	while (readNormalMember(&arch))
	{
		/*
		 * If this file is not wanted then skip it.
		 */
		if (!wantMember(&arch, argc, argv))
		{
			if (!skipMember(&arch))
				break;

			continue;
		}

		/*
		 * This file is wanted.
		 */
		if (doTable)
		{
			if (verbose)
				listMember(&arch);
			else
				puts(arch.name);

			if (!skipMember(&arch))
				break;
		}
		else if (doPrint)
		{
			if (verbose)
			{
				/*
				 * The verbose format makes me gag,
				 * but 4.4BSD, GNU and even V7 all
				 * have the same lossage.
				 */
				printf("\n<%s>\n\n", arch.name);
				fflush(stdout);
			}

			if (!writeFile(&arch, STDOUT))
				break;
		}
		else if (doExtract)
		{
			int	outfd;
			BOOL	success;

			if (verbose)
				printf("x - %s\n", arch.name);

			outfd = createFile(&arch);

			if (outfd == -1)
				break;

			success = writeFile(&arch, outfd);

			if (close(outfd) == -1)
			{
				fprintf(stderr, "Can't close %s: %s\n",
					arch.name, strerror(errno));

				break;
			}

			if (!success)
				break;
		}
		else
		{
			fprintf(stderr, "Oops -- I don't know what to do\n");
			break;
		}
	}

	closeArchive(&arch);
}


/*
 * Open the file for writing for the specified archive structure,
 * setting its mode and owner if possible.  Returns the file handle
 * of the opened file, or -1 on an error.
 */
static int
createFile(const Archive * arch)
{
	int	fd;

	fd = open(arch->name, O_WRONLY | O_CREAT | O_TRUNC, arch->mode);

	if (fd == -1)
	{
		fprintf(stderr, "Can't create \"%s\": %s\n",
			arch->name, strerror(errno));

		return -1;
	}

	/*
	 * Don't worry if these fail.  We have to do the fchmod() despite
	 * specifying the mode in the open() call, because that mode is
	 * munged by the umask.
	 */
	(void) fchmod(fd, arch->mode);
	(void) fchown(fd, arch->uid, arch->gid);

	return fd;
}


/*
 * Return whether the current archive member is wanted.
 * This means that the file name is contained in the specified list of
 * file names, or else that the specified list of file names is empty.
 */
static BOOL
wantMember(const Archive * arch, int n_names, const char ** name)
{
	int	i;

	/*
	 * If there are no names then all archive members are wanted.
	 */
	if (n_names == 0)
		return TRUE;

	/*
	 * See if the member file name is contained in the list.
	 */
	for (i = 0; i < n_names; i++)
	{
		if (strcmp(arch->name, name[i]) == 0)
			return TRUE;
	}

	return FALSE;
}


/*
 * Parse a number from the specified string in the specified base.
 * The number is terminated by a null, space, or the specified number
 * of digits.  The number is returned through the supplied pointer.
 * Only non-negatives numbers are parsed.  Returns TRUE on success.
 */
static BOOL
getNumber(const char * s, unsigned base, int max_digits, unsigned long * ul)
{
	const char *	p;
	const char *	endp;
	unsigned long	cutoff;
	unsigned long	cutlim;

	if (base < 2 || base > 10  ||  s == 0 || *s == 0  ||  ul == 0)
		return FALSE;

	cutoff = ULONG_MAX / (unsigned long) base;
	cutlim = ULONG_MAX % (unsigned long) base;
	*ul = 0;

	endp = (max_digits >= 0) ? s + max_digits : 0;

	for (p = s;  endp ? p < endp : 1;  p++)
	{
		unsigned d;

		if (*p == 0 || *p == ' ')
			break;	/* end of string */

		if (!isDecimal(*p))
			return FALSE; /* non-digit */

		d = *p - '0';

		if (d >= base)
			return FALSE; /* digit outside range for base */

		if (*ul > cutoff || (*ul == cutoff && d > cutlim))
			return FALSE; /* overflow */

		*ul *= base;
		*ul += d;
	}

	if (p == s)
		return FALSE;	/* nothing was converted */

	if (*p && *p != ' ')
		return FALSE;	/* trailing garbage */

	return TRUE;
}


/*
 * Initialise the specified Archive structure for use.
 */
static void
initArchive(Archive * arch)
{
	arch->fd = -1;
	arch->name = 0;
	arch->nameTable = 0;
	arch->eof = FALSE;
	arch->rescan = FALSE;
}


/*
 * Open the specified archive file name and read the header from it.
 * The file handle is saved in the Archive structure for further use.
 * Returns TRUE on success.
 */
static BOOL
openArchive(const char * name, Archive * arch)
{
	unsigned char	buf[SARMAG];
	ssize_t		cc;

	arch->fd = open(name, O_RDONLY);

	if (arch->fd == -1)
	{
		fprintf(stderr, "Can't open archive file %s: %s\n",
			name, strerror(errno));

		return FALSE;
	}

	cc = read(arch->fd, buf, SARMAG);

	if (cc == -1)
	{
		fprintf(stderr, "Error reading archive header: %s\n",
			strerror(errno));

		goto close_and_out;
	} 

	if (cc != SARMAG)
	{
		fprintf(stderr, "Short read of archive header\n");

		goto close_and_out;
	}

	if (memcmp(buf, ARMAG, SARMAG))
	{
		fprintf(stderr, "Invalid archive header\n");

		goto close_and_out;
	}

	return TRUE;


	/*
	 * Here on an error to clean up.
	 */
close_and_out:
	(void) close(arch->fd);
	arch->fd = -1;

	return FALSE;
}


/*
 * Close the archive file.
 */
static void
closeArchive(Archive * arch)
{
	free(arch->name);
	arch->name = 0;

	free(arch->nameTable);
	arch->nameTable = 0;

	if (arch->fd >= 0)
		(void) close(arch->fd);

	arch->fd = -1;
}


/*
 * Read an archive member header into the specified structure.
 * Returns TRUE on success.  On failure, the eof flag is set if
 * the end of file had been reached.
 */
static BOOL
readMember(Archive * arch, struct ar_hdr * hdr)
{
	ssize_t	cc;

	cc = read(arch->fd, hdr, sizeof(*hdr));

	if (cc < 0)
	{
		fprintf(stderr, "Error reading member header: %s\n",
			strerror(errno));

		return FALSE;
	}

	if (cc == 0)
	{
		arch->eof = TRUE;

		return FALSE;
	}

	if (cc != sizeof(*hdr))
	{
		fprintf(stderr, "Short read of member header\n");

		return FALSE;
	}

	if (memcmp(hdr->ar_fmag, ARFMAG, sizeof(hdr->ar_fmag)))
	{
		fprintf(stderr, "Invalid member header\n");

		return FALSE;
	}

	return TRUE;
}


/*
 * Check the member file name and see if it matches the 4.4 BSD
 * syntax for long file names.  If so, return the number of characters
 * of the actual long file name.  Returns -1 on an error.
 */
static int
shortNameMatches44BSD(const char * name)
{
	const char *	p;
	unsigned long	ul;

	if (strncmp(name, "#1/", 3) != 0)
		return -1;

	if (!isDecimal(name[3]))
		return -1;

	for (p = name + 4;  *p;  p++)
	{
		if (!isDecimal(*p))
			break;
	}

	while (*p)
	{
		if (*p++ != ' ')
			return -1;
	}

	if (!getNumber(name + 3, 10, -1, &ul))
		return -1;

	if (ul == 0)		/* broken archive */
		return -1;

	return ul;
}


/*
 * Check the member file name and see if it matches the SYS V syntax
 * for long file names.  If so, return the number of characters of the
 * actual long file name.  Returns -1 on an error.
 */
static int
shortNameMatchesSysV(const char * name)
{
	const char *	p;
	unsigned long	ul;

	/* "^/(\d+) *$" */
	if (name[0] != '/')
		return -1;

	if (!isDecimal(name[1]))
		return -1;

	for (p = name + 2;  *p;  p++)
	{
		if (!isDecimal(*p))
			break;
	}

	while (*p)
	{
		if (*p++ != ' ')
			return -1;
	}

	if (!getNumber(name + 1, 10, -1, &ul))
		return -1;

	return ul;
}


#define MEMB_NAME_ALLOC(n)					\
	do							\
	{							\
		arch->name = malloc(n);				\
		if (!arch->name)				\
		{						\
			fprintf(stderr, "Out of memory\n");	\
			return FALSE;				\
		}						\
	} while (0);


/*
 * Examine the archive structure that was read and fill in the
 * current member data with the extracted information.  This handles
 * various types of archive headers.  This can read data from the
 * archive file to obtain a long file name.  Returns TRUE on success.
 */
static BOOL
canonicalize(Archive * arch, const struct ar_hdr * hdr)
{
	char		buf[sizeof(hdr->ar_name) + 1];
	int		n;
	unsigned long	ul;
	unsigned long	bsd_len;

	bsd_len = 0;

	free(arch->name);
	arch->name = 0;

	strncpy(buf, hdr->ar_name, sizeof(hdr->ar_name));
	buf[sizeof(hdr->ar_name)] = 0;

	/*
	 * 1. If shortname matches "^#1/(\d+) *$", then it's a 4.4BSD
	 *    longname.  Read a longname of $1 bytes from ARCH->fd, or
	 *    return FALSE if impossible.
	 */
	if ((n = shortNameMatches44BSD(buf)) != -1)
	{
		/* N is the length of the longname */
		ssize_t cc;

		bsd_len = n;

		MEMB_NAME_ALLOC(n + 1);

		cc = read(arch->fd, arch->name, n);

		if (cc == -1)
		{
			fprintf(stderr, "Error reading longname: %s\n",
				strerror(errno));

			goto free_and_out;
		}

		if (cc != n)
		{
			fprintf(stderr, "Unexpected end of file in longname\n");

			goto free_and_out;
		}

		arch->name[n] = 0;
	}

	/*
	 * 2. Otherwise, if shortname matches "^/(\d+) *$", then it's a SysV
	 *    longname.  Get the longname from the nameTable, or return FALSE
	 *    if there is none.
	 */
	else if ((n = shortNameMatchesSysV(buf)) != -1)
	{
		/*
		 * N is the index of the longname
		 */
		const char *	longname;
		const char *	p;
		size_t		len;

		if (n >= strlen(arch->nameTable))
		{
			fprintf(stderr, "Longname index too large\n");

			return FALSE;
		}

		longname = arch->nameTable + n;

		p = strchr(longname, '/');

		if (!p)
		{
			fprintf(stderr, "Bad longname index\n");

			return FALSE;
		}

		if (p[1] != '\n')
		{
			fprintf(stderr, "Malformed longname table\n");

			return FALSE;
		}

		len = p - longname;
		MEMB_NAME_ALLOC(len + 1);
		strncpy(arch->name, longname, len);
		arch->name[len] = '\0';
	}

	/*
	 * 3. Otherwise, it's just a shortname.  If the shortname contains a
	 *    slash, then the name terminates before the slash; otherwise,
	 *    the name terminates at the first space, or at the end of the
	 *    field if there is none. */
	else
	{
		const char *	p;
		size_t		len;

		p = strchr(buf, '/');

		if (!p)
			p = strchr(buf, ' ');

		if (p)
			len = p - buf;
		else
			len = sizeof(hdr->ar_name);

		MEMB_NAME_ALLOC(len + 1);
		strncpy(arch->name, buf, len);
		arch->name[len] = 0;
	}

	/*
	 * 4. Parse the remaining fields of the header.  Return FALSE if any
	 *    are missing or ill-formed.
	 */
#define FIELD(AFIELD, MFIELD, base)					\
	if (!getNumber(hdr->AFIELD, base, sizeof(hdr->AFIELD), &ul))	\
	{								\
		fprintf(stderr, "Malformed archive member header\n");	\
		goto free_and_out;					\
	}								\
	arch->MFIELD = ul;

	FIELD(ar_date, date, 10);
	FIELD(ar_uid,  uid,  10);
	FIELD(ar_gid,  gid,  10);
	FIELD(ar_mode, mode,  8);
	FIELD(ar_size, size, 10);
#undef FIELD

	/*
	 * 4a. Decide whether a pad byte will be present.u
	 *
	 * The 4.4BSD format is really broken and needs a whole pile of
	 * cruft to deal with it.  There are several cases:
	 *
	 * 1. Even namelen, even memberlen: no pad.
	 * 2. Even namelen, odd memberlen: pad.  Just like SysV.
	 * 3. Odd namelen, even memberlen: pad.  Cruft.
	 * 4. Odd namelen, odd memberlen: no pad.  Cruft.
	 *
	 * Essentially, whenever the namelen is odd, the naive determination
	 * of whether a pad is needed is reversed.
	 */
	if (!bsd_len)
		arch->pad = (arch->size % 2) ? 1 : 0;
	else
	{
		arch->size -= bsd_len;
		arch->pad = (arch->size % 2) ? 1 : 0;

		if (bsd_len % 2)
			arch->pad = !arch->pad;
	}

	/*
	 * 5. Everything was successful.
	 */
	return TRUE;


	/*
	 * 5a. Error exit -- free memory.
	 */
free_and_out:
	free(arch->name);
	arch->name = 0;

	return FALSE;
}


/*
 * Skip the padding character if required to position to the
 * beginning of the next member header.  This is done if the
 * padding value is nonzero.  Returns TRUE on success.
 */
static BOOL
skipPadding(int fd, int pad)
{
	if (pad)
	{
		if (lseek(fd, 1, SEEK_CUR) == -1)
		{
			fprintf(stderr, "Can't skip pad byte: %s\n",
				strerror(errno));

			return FALSE;
		}
	}

	return TRUE;
}


/*
 * Read the first member of the archive file and check whether it
 * is a special one, and if so, handle it.  If the first member is
 * a normal archive member, then set up to rescan it for the next
 * readNormalMember call.  Returns TRUE on success.
 */
static BOOL
readSpecialMember(Archive * arch)
{
	struct ar_hdr	hdr;

	/*
	 * 1. Read a header H.  Fail if impossible.
	 */
	if (!readMember(arch, &hdr))
		return FALSE;

	/*
	 * 2. If H is a symbol table, ditch it.
	 * Fail if impossible.
	 */
	if ((strncmp(hdr.ar_name, "/ ", 2) == 0) ||
		(strncmp(hdr.ar_name, "__.SYMTAB       ",
			sizeof(hdr.ar_name)) == 0))
	{
		if (!canonicalize(arch, &hdr))
			return FALSE;

		return skipMember(arch);
	}

	/*
	 * 3. If H is a SysV longname table, read it into ARCH.
	 */
	if (strncmp(hdr.ar_name, "//", 2) == 0)
	{
		unsigned long	len;
		ssize_t		cc;

		if (!getNumber(hdr.ar_size, 10, sizeof(hdr.ar_size), &len))
		{
			fprintf(stderr, "Invalid name-table size\n");

			return FALSE;
		}

		arch->nameTable = malloc(len + 1);

		if (!arch->nameTable)
		{
			fprintf(stderr, "Out of memory\n");

			return FALSE;
		}

		cc = read(arch->fd, arch->nameTable, len);

		if (cc == -1)
		{
			fprintf(stderr, "Error reading name-table: %s\n",
				strerror(errno));

			return FALSE;
		}

		if (cc != (ssize_t) len)
		{
			fprintf(stderr,
				"Unexpected end of file in name-table\n");

			return FALSE;
		}

		arch->nameTable[len] = 0;

		return skipPadding(arch->fd, len % 2);
	}

	/*
	 * 4. We read a normal header.
	 * Canonicalize it, and mark it as needing rescanning.
	 */
	arch->rescan = TRUE;

	return canonicalize(arch, &hdr);
}


/*
 * Read the next normal member of the archive file if possible.
 * If the member is being rescanned, clear the rescan flag and
 * return the header that was already read.  Returns TRUE on
 * success.  On a failure, the eof flag will be set if end of
 * file has been reached.
 */
static BOOL
readNormalMember(Archive * arch)
{
	struct ar_hdr	hdr;

	/*
	 * If we are rereading an old header then just clear the
	 * rescan flag and return success.
	 */
	if (arch->rescan)
	{
		arch->rescan = FALSE;

		return TRUE;
	}

	/*
	 * We need to read a new member header.
	 */
	if (!readMember(arch, &hdr))
		return FALSE;

	return canonicalize(arch, &hdr);
}


/*
 * Skip the current member of the archive so that we are positioned
 * to tbe beginning of the next member's header (or end of file).
 * Returns TRUE on success.
 */
static BOOL
skipMember(const Archive * arch)
{
	if (lseek(arch->fd, arch->size, SEEK_CUR) == -1)
	{
		fprintf(stderr, "Can't skip past archive member: %s\n",
			strerror(errno));

		return FALSE;
	}

	return skipPadding(arch->fd, arch->pad);
}


/*
 * Copy all of the file data from the archive to the specified
 * open file.  Returns TRUE on success.
 */
static BOOL
writeFile(const Archive * arch, int outfd)
{
	unsigned char	buf[BUF_SIZE];
	off_t		n;

	n = arch->size;

	while (n > 0)
	{
		ssize_t cc;

		cc = read(arch->fd, buf, MIN(n, sizeof(buf)));

		if (cc == -1)
		{
			fprintf(stderr, "Error reading archive member: %s\n",
				strerror(errno));

			return FALSE;
		}

		if (cc == 0)
		{
			fprintf(stderr, "Unexpected end of file\n");

			return FALSE;
		}

		if (fullWrite(outfd, buf, cc) < 0)
		{
			fprintf(stderr, "Write error: %s\n", strerror(errno));

			return FALSE;
		}

		n -= cc;
	}

	if (!skipPadding(arch->fd, arch->pad))
		return FALSE;

	return TRUE;
}


/*
 * Print one line listing the information about the specified archive member.
 */
static void
listMember(const Archive * arch)
{
	printf("%s %6ld/%-6ld %8lu %s %s\n",
	       modeString(arch->mode) + 1,
	       (long) arch->uid,
	       (long) arch->gid,
	       (unsigned long) arch->size,
	       timeString(arch->date),
	       arch->name);
}


/* END CODE */
