/*
 * cbmarcs.c
 *
 * for fvcbm ver. 2.0 by Dan Fandrich
 *
 * Commodore archive formats directory display routines
 *
 * Compile this file with "pack structures" compiler flag if not GNU C
 */

#ifdef __MSDOS__
#include <io.h>
#else
extern long filelength(int);
#endif
#include <string.h>
#include <ctype.h>
#include "cbmarcs.h"

#if defined(BIG_ENDIAN) || (WORDS_BIG_ENDIAN==1)
#error cbmarcs.c requires a little-endian CPU
#endif

typedef unsigned char BYTE;		/* 8 bits */
typedef unsigned short WORD;	/* 16 bits */
typedef unsigned long LONG;		/* 32 bits */

#ifdef __GNUC__
#define PACK __attribute__ ((packed))	/* pack structures on byte boundaries */
#else
#define PACK
#endif

extern char *ProgName;

/******************************************************************************
* Constants
******************************************************************************/

/* These archive format signatures are somewhat of a kludge */

static BYTE MagicHeaderC64[10] = {0x9e,'(','2','0','6','3',')',0x00,0x00,0x00};
static BYTE MagicHeaderC128[10] = {0x9e,'(','7','1','8','3',')',0x00,0x00,0x00};
static BYTE MagicHeaderARC = 2;
static BYTE MagicHeaderLHASFX[10] = {0x97,0x32,0x30,0x2C,0x30,0x3A,0x8B,0xC2,0x28,0x32};
static BYTE MagicHeaderLHA[3] = {'-','l','h'};
static BYTE MagicHeaderLynx[10] = {' ','1',' ',' ',' ','L','Y','N','X',' '};
static BYTE MagicHeaderLynxNew[27] = {0x0A,0x00,0x97,'5','3','2','8','0',',','0',0x3A,
							   0x97,'5','3','2','8','1',',','0',0x3A,
							   0x97,'6','4','6',',',0xC2,0x28};
static BYTE MagicHeaderT64[19] = {'C','6','4',' ','t','a','p','e',' ',
						   'i','m','a','g','e',' ','f','i','l','e'};
static BYTE MagicHeaderD64[3] = {'C','B','M'};
static BYTE MagicHeaderX64[4] = {0x43,0x15,0x41,0x64};
static BYTE MagicHeaderP00[8] = {'C','6','4','F','i','l','e',0};

static BYTE MagicC64_10[3] = {0x85,0xfd,0xa9};
static BYTE MagicC64_13[3] = {0x85,0x2f,0xa9};
static BYTE MagicC64_15[4] = {0x8d,0x21,0xd0,0x4c};
static BYTE MagicC128_15 = 0x4c;

enum {MagicARCEntry = 2};

static BYTE MagicLHAEntry[3] = {'-','l','h'};

/* These descriptions must be in the order encountered in ArchiveTypes */
char *ArchiveFormats[] = {
/* C64__0 */	" ARC",
/* C64_10 */	" C64",
/* C64_13 */	" C64",
/* C64_15 */	" C64",
/* C128_15 */	"C128",
/* LHA_SFX */	" LHA",
/* LHA */		" LHA",
/* Lynx */		"Lynx",
/* New Lynx */	"Lynx",
/* Tape image */" T64",
/* Disk image */" D64",
/* Disk image */" X64",
/* PRG file */	" P00",
/* SEQ file */	" S00",
/* USR file */	" U00",
/* REL file */	" R00",
/* DEL file */	" D00",
/* P00-like */	"P00?"
};

/* CBM ARC compression types */
static char *ARCEntryTypes[] = {
/* 0 */	"Stored",
/* 1 */ "Packed",
/* 2 */ "Squeezed",
/* 3 */ "Crunched",
/* 4 */ "Squashed",
/* 5 */ "?5",			/* future use */
/* 6 */ "?6",
/* 7 */ "?7",
/* 8 */ "?8"
};

/* LHA compression types */
static char *LHAEntryTypes[] = {
/* 0 */	"Stored",
/* 1 */ "lh1",
/* 2 */ "lh2",
/* 3 */ "lh3",
/* 4 */ "lh4",
/* 5 */ "lh5",
/* 6 */ "lh6",
/* 7 */ "lh7",
/* 8 */ "lh8",
/* 9 */ "lh9",
/* A */ "lhA",
/* B */ "lhB"
};

/* File types in T64 tape archives */
static char *T64FileTypes[] = {
/* 0 */	"SEQ",
/* 1 */ "PRG",
/* 2 */ "?2?",
/* 3 */ "?3?",
/* 4 */ "?4?",
/* 5 */ "?5?",
/* 6 */ "?6?",
/* 7 */ "?7?"
};

/* Disk types in X64 disk images */
enum {
	X64_1541 = 0		/* 1541 disk image */
};

/* File types as found on disk (bitwise AND code with with CBM_TYPE) */
static char *CBMFileTypes[] = {
/* 0 */	"DEL",
/* 1 */	"SEQ",
/* 2 */ "PRG",
/* 3 */ "USR",
/* 4 */ "REL",

/* 5 */ "?5?",	/* should never see the rest */
/* 6 */ "?6?",
/* 7 */ "?7?"
};

/* File type mask bits */
enum {
	CBM_TYPE = 0x07,		/* Mask to get file type */
	CBM_CLOSED = 0x80,		/* Mask to get closed bit */
	CBM_LOCKED = 0x40		/* Mask to get locked bit */
};

/* End of 1541 filename character */
enum { CBM_END_NAME = '\xA0' };

/******************************************************************************
* Structures
******************************************************************************/

/* Struct containing enough information to determine the type of archive given
   any file */
struct ArchiveHeader {
	union {
		struct {
			WORD StartAddress PACK;
			BYTE Filler1[2] PACK;
			WORD Version PACK;
			BYTE Magic1[10] PACK;

			BYTE Filler2 PACK;
			BYTE FirstOffL PACK;
			BYTE Magic2[3] PACK;
			BYTE FirstOffH PACK;
		} C64_10;

		struct {
			WORD StartAddress PACK;
			BYTE Filler1[2] PACK;
			WORD Version PACK;
			BYTE Magic1[10] PACK;

			BYTE Filler2[11] PACK;
			BYTE FirstOffL PACK;
			BYTE Magic2[3] PACK;
			BYTE FirstOffH PACK;
		} C64_13;

		struct {
			WORD StartAddress PACK;
			BYTE Filler1[2] PACK;
			WORD Version PACK;
			BYTE Magic1[10] PACK;

			BYTE Filler2[7] PACK;
			BYTE Magic2[4] PACK;
			WORD StartPointer PACK;
		} C64_15;

		struct {
			WORD StartAddress PACK;
			BYTE Filler1[2] PACK;
			WORD Version PACK;
			BYTE Magic1[10] PACK;

			BYTE Magic2 PACK;
			WORD StartPointer PACK;
		} C128_15;

		struct {
			BYTE Magic PACK;
			BYTE EntryType PACK;
		} C64_ARC;

		struct {
			WORD StartAddress PACK;
			BYTE Filler[4] PACK;
			BYTE Magic[sizeof(MagicHeaderLHASFX)] PACK;
		} LHA_SFX;

		struct {
			BYTE Filler[2] PACK;
			BYTE Magic[sizeof(MagicHeaderLHA)] PACK;
		} LHA;

		struct {
			BYTE Magic[sizeof(MagicHeaderLynx)] PACK;
		} Lynx;

		struct {
			WORD StartAddress PACK;
			WORD EndHeaderAddr PACK;
			BYTE Magic[sizeof(MagicHeaderLynxNew)] PACK;
		} LynxNew;

		struct {
			BYTE Magic[sizeof(MagicHeaderT64)] PACK;
		} T64;

		struct {
			BYTE Magic[sizeof(MagicHeaderD64)] PACK;
		} D64;

		struct {
			BYTE Magic[sizeof(MagicHeaderX64)] PACK;
		} X64;

		struct {
			BYTE Magic[sizeof(MagicHeaderP00)] PACK;
		} P00;
	} Type;
};

struct ArchiveHeaderNew {
	BYTE Filler1 PACK;
	BYTE FirstOffL PACK;
	BYTE Filler2[3] PACK;
	BYTE FirstOffH PACK;
};

struct ArchiveEntryHeader {
	BYTE Magic PACK;
	BYTE EntryType PACK;
	WORD Checksum PACK;
	WORD LengthL PACK;
	BYTE LengthH PACK;
	BYTE BlockLength PACK;
	BYTE Filler PACK;
	BYTE FileType PACK;
	BYTE FileNameLen PACK;
};

struct LHAEntryHeader {
	BYTE HeadSize PACK;
	BYTE HeadChk PACK;
	BYTE HeadID[3] PACK;
	BYTE EntryType PACK;
	BYTE Magic PACK;
	LONG PackSize PACK;
	LONG OrigSize PACK;
	struct	{			/* DOS format time */
		WORD ft_tsec : 5;	/* Two second interval */
		WORD ft_min	 : 6;	/* Minutes */
		WORD ft_hour : 5;	/* Hours */
		WORD ft_day	 : 5;	/* Days */
		WORD ft_month: 4;	/* Months */
		WORD ft_year : 7;	/* Year */
	} FileTime PACK;
	WORD Attr PACK;
	BYTE FileNameLen PACK;
	BYTE FileName[64] PACK;
};

struct T64Header {
	BYTE Magic[32] PACK;
	BYTE MinorVersion PACK;
	BYTE MajorVersion PACK;
	WORD Entries PACK;
	WORD Used PACK;
	WORD unused PACK;
	BYTE TapeName[24] PACK;
};

struct T64EntryHeader {
	BYTE EntryType PACK;
	BYTE FileType PACK;
	WORD StartAddr PACK;
	WORD EndAddr PACK;
	WORD unused1 PACK;
	LONG FileOffset PACK;
	LONG unused2 PACK;
	BYTE FileName[16] PACK;
};

struct X64Header {
	BYTE Magic[4] PACK;
	BYTE MajorVersion PACK;
	BYTE MinorVersion PACK;
	BYTE DeviceType PACK;
	BYTE MaxTracks PACK;	/* versions >= 1.2 only */
};

struct D64DirHeader {
	BYTE FirstTrack PACK;
	BYTE FirstSector PACK;
	BYTE Format PACK;
	BYTE Reserved PACK;
	BYTE BAM[140] PACK;
	BYTE DiskName[18] PACK;
	BYTE Filler1 PACK;
	BYTE DOSVersion PACK;
	BYTE DOSFormat PACK;
	BYTE Filler2[4] PACK;
  /*BYTE Filler3[85] PACK;*/
};

struct D64EntryHeader {
	BYTE FileType PACK;
	BYTE FirstTrack PACK;
	BYTE FirstSector PACK;
	BYTE FileName[16] PACK;
	BYTE FirstSideTrack PACK;
	BYTE FirstSideSector PACK;
	BYTE RecordSize PACK;
	BYTE Filler1[4] PACK;
	BYTE FirstReplacementTrack PACK;
	BYTE FirstReplacementSector PACK;
	WORD FileBlocks PACK;
	WORD Filler2 PACK;
};

enum { D64_ENTRIES_PER_BLOCK = 8 };

struct D64DirBlock {
	BYTE NextTrack PACK;
	BYTE NextSector PACK;
	struct D64EntryHeader Entry[D64_ENTRIES_PER_BLOCK] PACK;
};

struct D64DataBlock {
	BYTE NextTrack PACK;
	BYTE NextSector PACK;
/*	BYTE Data[]; */		/* GNU C doesn't like this line, but we don't need it */
};

struct P00Header {
	BYTE Magic[8] PACK;
	BYTE FileName[17] PACK;
	BYTE RecordSize PACK;		/* REL file record size */
};

/******************************************************************************
* Functions
******************************************************************************/

/******************************************************************************
* Return file type string given letter code
******************************************************************************/
static char *FileTypes(char TypeCode)
{
	switch (TypeCode) {
		case 'P': return "PRG";
		case 'S': return "SEQ";
		case 'U': return "USR";
		case 'R': return "REL";
		case 'D': return "DEL";
		case ' ': return "   ";
		default:  return "???";
	}
}

/******************************************************************************
* Return smallest of 2 numbers
******************************************************************************/
#ifndef min
#define min(a,b)        (((a) < (b)) ? (a) : (b))
#endif

/******************************************************************************
* Convert CBM file names into ASCII strings
* Converts in place & returns pointer to start of string
******************************************************************************/
static char *ConvertCBMName(char *InString)
{
	char *LastNonBlank = InString;
	char *Ch;

	for (Ch = InString; *Ch; ++Ch) {
		*Ch = *Ch & 0x7F;		/* strip high bit */
		if (!isspace(*Ch))
			LastNonBlank = Ch;
	}
	*++LastNonBlank = 0;		/* truncate string after last character */
	return InString;
}

/******************************************************************************
* Convert Roman numeral to decimal
* Note: input string is cleared
******************************************************************************/
static int RomanToDec(char *Roman)
{
	int Digit;
	char Last = 0;
	int Value = 0;

	while (*Roman) {
		switch (toupper(*Roman)) {
			case 'I': Digit = 1; break;
			case 'V': Digit = 5; break;
			case 'X': Digit = 10; break;
			case 'L': Digit = 50; break;
			case 'C': Digit = 100; break;
		}
		Value = Last < Digit ? Digit - Value: Value + RomanToDec(Roman);
		Last = Digit;
		*Roman++ = 0;
	}
	return Value;
}

/******************************************************************************
* Return disk image offset for 1541 disk
******************************************************************************/
static unsigned long Location1541TS(unsigned char Track, unsigned char Sector)
{
	static const unsigned Sectors[42] = {
	/* Tracks number	Offset in sectors of start of track */
	/* tracks 1-18 */	0,21,42,63,84,105,126,147,168,189,
						210,231,252,273,294,315,336,357,
	/* tracks 19-25 */	376,395,414,433,452,471,490,
	/* tracks 26-31 */	508,526,544,562,580,598,
	/* tracks 32-35 */	615,632,649,666,
	/* The rest of the tracks are nonstandard */
	/* tracks 36-42 */	683,700,717,734,751,768,785
	};
	enum {BYTES_PER_SECTOR=256};	/* bytes per sector in 1541 disk image */

	return (Sectors[Track-1] + Sector) * (long) BYTES_PER_SECTOR;
}

/******************************************************************************
* Follow chain of file sectors in disk image, counting total bytes in the file
******************************************************************************/
static unsigned long CountCBMBytes(FILE *DiskImage, unsigned long Offset,
						unsigned char FirstTrack, unsigned char FirstSector)
{
	struct D64DataBlock DataBlock;
	unsigned int BlockCount = 0;

	DataBlock.NextTrack = FirstTrack;		/* prime the track & sector */
	DataBlock.NextSector = FirstSector;
	do {
		if (fseek(DiskImage, Location1541TS(
								DataBlock.NextTrack,
								DataBlock.NextSector
							 ) + Offset, SEEK_SET) != 0) {
			perror(ProgName);
			return 2;
		}
		if (fread(&DataBlock, sizeof(DataBlock), 1, DiskImage) != 1) {
			perror(ProgName);
			return 2;
		}
		++BlockCount;
	} while (DataBlock.NextTrack > 0);

	return (BlockCount - 1) * 254 + DataBlock.NextSector - 1;
}


/******************************************************************************
* ARC reading routines
******************************************************************************/
int DirARC(FILE *InFile, enum ArchiveTypes ArcType,	struct ArcTotals *Totals,
		int (DisplayFunction)()) {
	long CurrentPos;
	char EntryName[17];
	long FileLen;
	struct ArchiveEntryHeader FileHeader;
	struct ArchiveHeaderNew FileHeaderNew;
	struct ArchiveHeader Header;

	Totals->ArchiveEntries = 0;
	Totals->TotalBlocks = 0;
	Totals->TotalBlocksNow = 0;
	Totals->TotalLength = 0;
	Totals->DearcerBlocks = 0;
	Totals->Version = 0;

	if (fseek(InFile, 0, SEEK_SET) != 0) {
		perror(ProgName);
		return 2;
	}

/******************************************************************************
* Reread header in order to get location of start of archives
* This might not be necessary, in that the version number may uniquely
*  determine the location of the start of data
******************************************************************************/
	if (fread(&Header, sizeof(Header), 1, InFile) != 1) {
		perror(ProgName);
		return 2;
	}

/******************************************************************************
* Find the version number and first archive entry offset for each format
******************************************************************************/
	switch (ArcType) {
		case C64__0:	/* Not a self dearcer -- just the arc data */
			CurrentPos = 0L;
			break;

		case C64_10:
			CurrentPos = ((Header.Type.C64_10.FirstOffH << 8) | Header.Type.C64_10.FirstOffL) - Header.Type.C64_10.StartAddress + 2;
			Totals->Version = -Header.Type.C64_10.Version;
			Totals->DearcerBlocks = (int) ((CurrentPos-1) / 254 + 1);
			break;

		case C64_13:
			CurrentPos = ((Header.Type.C64_13.FirstOffH << 8) | Header.Type.C64_13.FirstOffL) - Header.Type.C64_13.StartAddress + 2;
			Totals->Version = -Header.Type.C64_13.Version;
			Totals->DearcerBlocks = (int) ((CurrentPos-1) / 254 + 1);
			break;

		case C64_15:
			fseek(InFile, Header.Type.C64_15.StartPointer - Header.Type.C64_15.StartAddress + 2, SEEK_SET);
			fread(&FileHeaderNew, sizeof(FileHeaderNew), 1, InFile);
			CurrentPos = ((FileHeaderNew.FirstOffH << 8) | FileHeaderNew.FirstOffL) - Header.Type.C64_15.StartAddress + 2;
			Totals->Version = -Header.Type.C64_15.Version;
			Totals->DearcerBlocks = (int) ((CurrentPos-1) / 254 + 1);
			break;

		case C128_15:
			fseek(InFile, Header.Type.C128_15.StartPointer - Header.Type.C128_15.StartAddress + 2, SEEK_SET);
			fread(&FileHeaderNew, sizeof(FileHeaderNew), 1, InFile);
			CurrentPos = ((FileHeaderNew.FirstOffH << 8) | FileHeaderNew.FirstOffL) - Header.Type.C128_15.StartAddress + 2;
			Totals->Version = -Header.Type.C128_15.Version;
			Totals->DearcerBlocks = (int) ((CurrentPos-1) / 254 + 1);
			break;
	}

/******************************************************************************
* Read the archive directory contents
******************************************************************************/
	if (fseek(InFile, CurrentPos, SEEK_SET) != 0) {
		perror(ProgName);
		return 2;
	}

	while (1) {
		if (fread(&FileHeader, sizeof(FileHeader), 1, InFile) != 1)
			break;
		if (FileHeader.Magic != MagicARCEntry)
			break;
		fread(&EntryName, FileHeader.FileNameLen, 1, InFile);
		EntryName[FileHeader.FileNameLen] = 0;

		FileLen = (long) (FileHeader.LengthH << 16L) | FileHeader.LengthL;
		DisplayFunction(
			ConvertCBMName(EntryName),
			FileTypes(FileHeader.FileType),
			(long) FileLen,
			(unsigned) ((FileLen-1) / 254 + 1),
			ARCEntryTypes[FileHeader.EntryType],
			(int) (100 - (FileHeader.BlockLength * 100L / (FileLen / 254 + 1))),
			(unsigned) FileHeader.BlockLength,
			(long) FileHeader.Checksum
		);

		CurrentPos += FileHeader.BlockLength * 254;
		fseek(InFile, CurrentPos, SEEK_SET);
		++Totals->ArchiveEntries;
		Totals->TotalLength += FileLen;
		Totals->TotalBlocks += (int) ((FileLen-1) / 254 + 1);
		Totals->TotalBlocksNow += FileHeader.BlockLength;
	};
	return 0;
}

/******************************************************************************
* Lynx reading routines
******************************************************************************/
int DirLynx(FILE *InFile, enum ArchiveTypes LynxType, struct ArcTotals *Totals,
		int (DisplayFunction)()) {
	char EntryName[17];
	char FileType[2];
	int NumFiles;
	int FileBlocks;
	int LastBlockSize;
	long FileLen;
	char LynxVer[10];
	int ExpectLastLength;
	int ReadCount;

	Totals->ArchiveEntries = 0;
	Totals->TotalBlocks = 0;
	Totals->TotalBlocksNow = 0;
	Totals->TotalLength = 0;
	Totals->DearcerBlocks = 0;
	Totals->Version = 0;

/******************************************************************************
* Find the version number and first archive entry offset for each format
******************************************************************************/
	switch (LynxType) {
		case Lynx:
			if (fseek(InFile, 0, SEEK_SET) != 0) {
				perror(ProgName);
				return 2;
			}
			if (fscanf(InFile, " %*s LYNX %s %*[^\015]", LynxVer) != 1) {
				perror(ProgName);
				return 2;
			}
			getc(InFile);				/* Get CR without killing whitespace */
			Totals->Version = RomanToDec(LynxVer);
			Totals->DearcerBlocks = 0;
			ExpectLastLength = Totals->Version >= 10;
			break;

		case LynxNew:
/*			fseek(InFile, Header.Type.LynxNew.EndHeaderAddr -
						Header.Type.LynxNew.StartAddress + 5, SEEK_SET); */
			if (fseek(InFile, 0x5F, SEEK_SET) != 0) {
				perror(ProgName);
				return 2;
			}
			if (fscanf(InFile, " %*s *LYNX %s %*[^\015]", LynxVer) != 1) {
				perror(ProgName);
				return 2;
			}
			getc(InFile);				/* Get CR without killing whitespace */
			Totals->Version = RomanToDec(LynxVer);
			Totals->DearcerBlocks = 0;
			ExpectLastLength = Totals->Version >= 10;
			break;
	}

	if (fscanf(InFile, "%d%*[^\015]\015", &NumFiles) != 1) {
		perror(ProgName);
		return 2;
	}

/******************************************************************************
* Read the archive directory contents
******************************************************************************/
	for (; NumFiles--;) {
		ReadCount = fscanf(InFile, "%16[^\015]%*[^\015]\015", EntryName);
		ReadCount += fscanf(InFile, "%d%*[^\015]\015", &FileBlocks);
		ReadCount += fscanf(InFile, "%1s%*[^\015]", FileType);
		if (ReadCount != 3) {
			perror(ProgName);
			return 2;
		}
		getc(InFile);	/* eat the CR (\015) without killing whitespace so
						   ftell() will be correct, below */

/******************************************************************************
* Find the exact length of the file.
* For the first n-1 entries (for all n entries in newer Lynx versions, like
*  XVII), the length in bytes of the last block of the file is specified.
* For the last entry in older Lynx versions, like IX, we must find out how many
*  bytes in the file and subtract everything not belonging to the last file.
*  This can give an incorrect result if the file has padding after the last
*  file (which would happen if the file was transferred using XMODEM), but
*  Lynx thinks the padding is part of the file, too.
******************************************************************************/
		if (NumFiles || ExpectLastLength) {
			fscanf(InFile, "%d%*[^\015]\015", &LastBlockSize);
			FileLen = (long) ((FileBlocks-1) * 254L + LastBlockSize - 1);
		} else				/* last entry -- calculate based on file size */
			FileLen = filelength(fileno(InFile)) - Totals->TotalBlocksNow * 254L -
							(((ftell(InFile) - 1) / 254) + 1) * 254L;

		DisplayFunction(
			ConvertCBMName(EntryName),
			FileTypes(FileType[0]),
			(long) FileLen,
			FileBlocks,
			"Stored",
			0,
			FileBlocks,
			-1L
		);

		++Totals->ArchiveEntries;
		Totals->TotalLength += FileLen;
		/* The following two values should equal */
		Totals->TotalBlocks += (int) ((FileLen-1) / 254 + 1);
		Totals->TotalBlocksNow += FileBlocks;
	};
	return 0;
}

/******************************************************************************
* LHA (SFX) reading routines
******************************************************************************/
int DirLHA(FILE *InFile, enum ArchiveTypes LHAType, struct ArcTotals *Totals,
		int (DisplayFunction)()) {
	struct LHAEntryHeader FileHeader;
	char FileName[80];
	long CurrentPos;

	Totals->ArchiveEntries = 0;
	Totals->TotalBlocks = 0;
	Totals->TotalBlocksNow = 0;
	Totals->TotalLength = 0;

/******************************************************************************
* Find the version number and first archive entry offset for each format
******************************************************************************/
	switch (LHAType) {
		case LHA_SFX:
			CurrentPos = 0xE89;		/* Must be a better way than this */
			Totals->Version = 0;
			Totals->DearcerBlocks = (int) ((CurrentPos-1) / 254 + 1);
			break;

		case LHA:
			CurrentPos = 0;
			Totals->Version = 0;
			Totals->DearcerBlocks = 0;
			break;
	}

/******************************************************************************
* Read the archive directory contents
******************************************************************************/
	if (fseek(InFile, CurrentPos, SEEK_SET) != 0) {
		perror(ProgName);
		return 2;
	}

	while (1) {
		if (fread(&FileHeader, sizeof(FileHeader), 1, InFile) != 1)
			break;
		if (memcmp(FileHeader.HeadID, MagicLHAEntry, sizeof(MagicLHAEntry)) != 0)
			break;

		memcpy(FileName, FileHeader.FileName, min(sizeof(FileName)-1, FileHeader.FileNameLen));
		FileName[min(sizeof(FileName)-1, FileHeader.FileNameLen)] = 0;
		DisplayFunction(
			ConvertCBMName(FileName),
			FileTypes(FileHeader.FileName[FileHeader.FileNameLen-2] ? ' ' : FileHeader.FileName[FileHeader.FileNameLen-1]),
			(long) FileHeader.OrigSize,
			FileHeader.OrigSize ? (unsigned) ((FileHeader.OrigSize-1) / 254 + 1) : 0,
			LHAEntryTypes[FileHeader.EntryType - '0'],
			FileHeader.OrigSize ? (int) (100 - (FileHeader.PackSize * 100L / FileHeader.OrigSize)) : 100,
			FileHeader.PackSize ? (unsigned) ((FileHeader.PackSize-1) / 254 + 1) : 0,
			(long) (unsigned) (FileHeader.FileName[FileHeader.FileNameLen+1] << 8) | FileHeader.FileName[FileHeader.FileNameLen]
		);

		CurrentPos += FileHeader.HeadSize + FileHeader.PackSize + 2;
		fseek(InFile, CurrentPos, SEEK_SET);
		++Totals->ArchiveEntries;
		Totals->TotalLength += FileHeader.OrigSize;
		Totals->TotalBlocks += (int) ((FileHeader.OrigSize-1) / 254 + 1);
		Totals->TotalBlocksNow += (int) ((FileHeader.PackSize-1) / 254 + 1);
	};
	return 0;
}


/******************************************************************************
* T64 reading routines
******************************************************************************/
int DirT64(FILE *InFile, enum ArchiveTypes ArchiveType, struct ArcTotals *Totals,
		int (DisplayFunction)()) {
	char FileName[17];
	int NumFiles;
	unsigned FileLength;
	struct T64Header Header;
	struct T64EntryHeader FileHeader;

	Totals->ArchiveEntries = 0;
	Totals->TotalBlocks = 0;
	Totals->TotalBlocksNow = 0;
	Totals->TotalLength = 0;
	Totals->DearcerBlocks = 0;
	Totals->Version = 0;

	if (fseek(InFile, 0, SEEK_SET) != 0) {
		perror(ProgName);
		return 2;
	}
	if (fread(&Header, sizeof(Header), 1, InFile) != 1) {
		perror(ProgName);
		return 2;
	}
	Totals->Version = -(Header.MajorVersion * 10 + Header.MinorVersion);
	Totals->ArchiveEntries = Header.Entries;

/******************************************************************************
* Read the archive directory contents
******************************************************************************/
	for (NumFiles = Header.Entries; NumFiles; --NumFiles) {
		if (fread(&FileHeader, sizeof(FileHeader), 1, InFile) != 1)
			break;

		memcpy(FileName, FileHeader.FileName, 16);
		FileName[16] = 0;
		FileLength = FileHeader.EndAddr - FileHeader.StartAddr + 2;
		DisplayFunction(
			ConvertCBMName(FileName),
			T64FileTypes[FileHeader.FileType],
			(long) FileLength,
			(unsigned) (FileLength / 254 + 1),
			"Stored",
			0,
			(unsigned) (FileLength / 254 + 1),
			(long) -1L
		);

		Totals->TotalLength += FileLength;
		Totals->TotalBlocks += (int) (FileLength / 254 + 1);
	}

	Totals->TotalBlocksNow = Totals->TotalBlocks;
	return 0;
}


/******************************************************************************
* D64/X64 reading routines
******************************************************************************/
int DirD64(FILE *InFile, enum ArchiveTypes D64Type, struct ArcTotals *Totals,
		int (DisplayFunction)()) {
	char FileName[17];
	char *EndName;
	long CurrentPos;
	long HeaderOffset;
	long FileLength;
	int EntryCount;
	struct D64DirHeader DirHeader;
	struct D64DirBlock DirBlock;
	struct X64Header Header;

	Totals->ArchiveEntries = 0;
	Totals->TotalBlocks = 0;
	Totals->TotalBlocksNow = 0;
	Totals->TotalLength = 0;
	Totals->DearcerBlocks = 0;
	Totals->Version = 0;

/******************************************************************************
* Find the version number and first archive entry offset for each format
******************************************************************************/
	switch (D64Type) {
		case D64:
			HeaderOffset = 0;			/* No header on D64 images */
			CurrentPos = Location1541TS(18,0);
			break;

		case X64:
			if (fseek(InFile, 0, SEEK_SET) != 0) {
				perror(ProgName);
				return 2;
			}
			if (fread(&Header, sizeof(Header), 1, InFile) != 1) {
				perror(ProgName);
				return 2;
			}
			if (Header.DeviceType != X64_1541) {
				fprintf(stderr,"%s: Unsupported disk image type (%d)\n",
						ProgName, Header.DeviceType);
				return 3;
			}

			Totals->Version = -(Header.MajorVersion * 10 +
								((Header.MinorVersion >= 10) ?
										Header.MinorVersion / 10 :
										Header.MinorVersion));

			/* Currently ignoring disk tracks from header -- assuming 1541 */
			HeaderOffset = 0x40;		/* X64 header takes 64 bytes */
			CurrentPos = Location1541TS(18,0) + HeaderOffset;
			break;
	}

/******************************************************************************
* Read the disk directory header block
******************************************************************************/
	if (fseek(InFile, CurrentPos, SEEK_SET) != 0) {
		perror(ProgName);
		return 2;
	}
	if (fread(&DirHeader, sizeof(DirHeader), 1, InFile) != 1) {
		perror(ProgName);
		return 2;
	}
	if (DirHeader.Format != 'A') {		/* Disk format code */
		fprintf(stderr,"%s: Unsupported disk image format (%c)\n",
				ProgName, DirHeader.Format);
		return 3;
	}

/******************************************************************************
* Go through the entire directory
******************************************************************************/
	/* Simulate having read a directory sector already */
	DirBlock.NextTrack = DirHeader.FirstTrack;
	DirBlock.NextSector = DirHeader.FirstSector;

	while (DirBlock.NextTrack > 0) {
		CurrentPos = Location1541TS(DirBlock.NextTrack, DirBlock.NextSector) + HeaderOffset;

		if (fseek(InFile, CurrentPos, SEEK_SET) != 0) {
			perror(ProgName);
			return 2;
		}
		if (fread(&DirBlock, sizeof(DirBlock), 1, InFile) != 1) {
			perror(ProgName);
			return 2;
		}

		for (EntryCount=0; EntryCount < D64_ENTRIES_PER_BLOCK; ++EntryCount) {
			if ((DirBlock.Entry[EntryCount].FileType & CBM_CLOSED) != 0) {

				FileLength = CountCBMBytes(
								InFile,
								HeaderOffset,
								DirBlock.Entry[EntryCount].FirstTrack,
								DirBlock.Entry[EntryCount].FirstSector
							 );
				strncpy(FileName, (char *) DirBlock.Entry[EntryCount].FileName, sizeof(FileName)-1);
				FileName[sizeof(FileName)-1] = 0;
				if ((EndName = strchr(FileName, CBM_END_NAME)) != NULL)
					*EndName = 0;

				DisplayFunction(
					ConvertCBMName(FileName),
					CBMFileTypes[DirBlock.Entry[EntryCount].FileType & CBM_TYPE],
					FileLength,
					DirBlock.Entry[EntryCount].FileBlocks,
					"Stored",
					0,
					DirBlock.Entry[EntryCount].FileBlocks,
					(long) -1L
				);
				Totals->TotalLength += FileLength;
				Totals->TotalBlocks += DirBlock.Entry[EntryCount].FileBlocks;
				++Totals->ArchiveEntries;
			}

		}
	}

	Totals->TotalBlocksNow = Totals->TotalBlocks;
	return 0;
}


/******************************************************************************
* P00,S00,U00,R00,D00 reading routines
******************************************************************************/
int DirP00(FILE *InFile, enum ArchiveTypes ArchiveType, struct ArcTotals *Totals,
		int (DisplayFunction)()) {
	long FileLength;
	char FileName[17];
	struct P00Header Header;
	char *FileType;

	Totals->ArchiveEntries = 0;
	Totals->TotalBlocks = 0;
	Totals->TotalBlocksNow = 0;
	Totals->TotalLength = 0;
	Totals->DearcerBlocks = 0;
	Totals->Version = 0;

/******************************************************************************
* P00 is just a regular file with a simple header prepended, so just read the
* header and display the name
******************************************************************************/
	if (fseek(InFile, 0, SEEK_SET) != 0) {
		perror(ProgName);
		return 2;
	}
	if (fread(&Header, sizeof(Header), 1, InFile) != 1) {
		perror(ProgName);
		return 2;
	}
	FileLength = filelength(fileno(InFile)) - sizeof(Header);
	strncpy(FileName, (char *) Header.FileName, 16);
	FileName[16] = 0;		/* never need this on a good P00 file */

	/* If archive type is unknown, see if file is REL */
	if ((ArchiveType == X00) && (Header.RecordSize > 0))
		ArchiveType = R00;

	switch (ArchiveType) {
		case S00: FileType = "SEQ"; break;
		case P00: FileType = "PRG"; break;
		case U00: FileType = "USR"; break;
		case R00: FileType = "REL"; break;
		case D00: FileType = "DEL"; break;
		case X00:
		default:  FileType = "???"; break;
	}

	DisplayFunction(
		ConvertCBMName(FileName),
		FileType,
		(long) FileLength,
		(unsigned) (FileLength / 254 + 1),
		"Stored",
		0,
		(unsigned) (FileLength / 254 + 1),
		(long) -1
	);

	Totals->ArchiveEntries = 1;
	Totals->TotalLength = FileLength;
	Totals->TotalBlocks = Totals->TotalBlocksNow = (int) (FileLength / 254 + 1);
	return 0;
}


/******************************************************************************
* Read the archive and determine which type it is
* File is already open; name is used for P00 etc. type detection
******************************************************************************/
enum ArchiveTypes DetermineArchiveType(FILE *InFile, const char *FileName)
{
	struct ArchiveHeader Header;
	enum ArchiveTypes ArchiveType;
	char *NameExt;

	if (fread(&Header, sizeof(Header), 1, InFile) != 1) {
		fprintf(stderr,"%s: Not a known Commodore archive\n", ProgName);
		return UnknownArchive;		/* Disk read error, probably file too short */
	}

/******************************************************************************
* Is it a C64 format?
******************************************************************************/
	if (memcmp(Header.Type.C64_10.Magic1, MagicHeaderC64, sizeof(MagicHeaderC64)) == 0) {
		if (memcmp(Header.Type.C64_10.Magic2, MagicC64_10, sizeof(MagicC64_10)) == 0)
			ArchiveType = C64_10;
		if (memcmp(Header.Type.C64_13.Magic2, MagicC64_13, sizeof(MagicC64_13)) == 0)
			ArchiveType = C64_13;
		if (memcmp(Header.Type.C64_15.Magic2, MagicC64_15, sizeof(MagicC64_15)) == 0)
			ArchiveType = C64_15;

/******************************************************************************
* Is it a C128 format?
******************************************************************************/
	} else if (memcmp(Header.Type.C128_15.Magic1, MagicHeaderC128, sizeof(MagicHeaderC128)) == 0) {
		if (Header.Type.C128_15.Magic2 == MagicC128_15)
			ArchiveType = C128_15;

/******************************************************************************
* Is it headerless ARC format?
* (This type does not have a dearcer built in, just the data)
******************************************************************************/
	} else if ((BYTE) Header.Type.C64_ARC.Magic == MagicHeaderARC) {
		ArchiveType = C64__0;

/******************************************************************************
* Is it LHA SFX format?
******************************************************************************/
	} else if (memcmp(Header.Type.LHA_SFX.Magic, MagicHeaderLHASFX, sizeof(MagicHeaderLHASFX)) == 0) {
		ArchiveType = LHA_SFX;

/******************************************************************************
* Is it headerless LHA format?
******************************************************************************/
	} else if (memcmp(Header.Type.LHA.Magic, MagicHeaderLHA, sizeof(MagicHeaderLHA)) == 0) {
		ArchiveType = LHA;

/******************************************************************************
* Is it Lynx format?
******************************************************************************/
	} else if (memcmp(Header.Type.Lynx.Magic, MagicHeaderLynx, sizeof(MagicHeaderLynx)) == 0) {
		ArchiveType = Lynx;

	} else if (memcmp(Header.Type.LynxNew.Magic, MagicHeaderLynxNew, sizeof(MagicHeaderLynxNew)) == 0) {
		ArchiveType = LynxNew;

/******************************************************************************
* Is it T64 format?
******************************************************************************/
	} else if (memcmp(Header.Type.T64.Magic, MagicHeaderT64, sizeof(MagicHeaderT64)) == 0) {
		ArchiveType = T64;

/******************************************************************************
* Is it D64 format?
* It appears the only good way to detect a D64 archive is to go look at
*  "track 18, sector 0" -- this way works on 1571-made autoboot disks (right?)
******************************************************************************/
	} else if (memcmp(Header.Type.D64.Magic, MagicHeaderD64, sizeof(MagicHeaderD64)) == 0) {
		ArchiveType = D64;

/******************************************************************************
* Is it X64 format?
******************************************************************************/
	} else if (memcmp(Header.Type.X64.Magic, MagicHeaderX64, sizeof(MagicHeaderX64)) == 0) {
		ArchiveType = X64;

/******************************************************************************
* Is it P00 format?
******************************************************************************/
	} else if (memcmp(Header.Type.P00.Magic, MagicHeaderP00, sizeof(MagicHeaderP00)) == 0) {
		ArchiveType = X00;		/* in case we can't determine type */
		if ((FileName != NULL) && ((NameExt = strrchr(FileName, '.')) != NULL))

			/* First letter of file extension gives archive type */
			switch (toupper(*++NameExt)) {
				case 'P': ArchiveType = P00; break;		/* PRG */
				case 'S': ArchiveType = S00; break;		/* SEQ */
				case 'U': ArchiveType = U00; break;		/* USR */
				case 'R': ArchiveType = R00; break;		/* REL */
				case 'D': ArchiveType = D00; break;		/* DEL */
				default:  ArchiveType = X00; break;		/* unknown */
			}

/******************************************************************************
* Unrecognized format
******************************************************************************/
	} else {
		ArchiveType = UnknownArchive;
		fprintf(stderr,"%s: Not a known Commodore archive\n", ProgName);
	}

	return ArchiveType;
}

/******************************************************************************
* Read and display the archive directory
******************************************************************************/
int DirArchive(FILE *InFile, enum ArchiveTypes ArchiveType,
		struct ArcTotals *Totals,
			int (DisplayFunction)())
{
/* Could put these functions into an array and call them using ArchiveType as index */
		switch (ArchiveType) {
			case C64__0:
			case C64_10:
			case C64_13:
			case C64_15:
			case C128_15:
				return DirARC(InFile, ArchiveType, Totals, DisplayFunction);

			case LHA_SFX:
			case LHA:
				return DirLHA(InFile, ArchiveType, Totals, DisplayFunction);

			case Lynx:
			case LynxNew:
				return DirLynx(InFile, ArchiveType, Totals, DisplayFunction);

			case T64:
				return DirT64(InFile, ArchiveType, Totals, DisplayFunction);

			case D64:
			case X64:
				return DirD64(InFile, ArchiveType, Totals, DisplayFunction);

			case P00:
			case S00:
			case U00:
			case R00:
			case D00:
			case X00:
				return DirP00(InFile, ArchiveType, Totals, DisplayFunction);

			default:		/* this should never happen */
				return 3;
		}
}
