/*
 * afid -- like the Apple ][ program called FID, a file handling utility
 *
 * version 1.1
 *
 * We operate on files which contain entire Apple ][ diskettes from
 * track 0, sector 0 to track 34, sector 15.  To keep things simple,
 * the entire disk image is slurped into memory.  I assume the standard
 * disk geometry of 35 tracks and 16 sectors when initializing, but
 * otherwise the code should operate on different geometries.  Different
 * geometries are a chicken and egg problem as the geometry info is
 * defined to be on track 0x11, sector 0, but you need the sector size
 * to figure out where track 0x11 is!  I have tried to make reasonable
 * extrapolations to account for different geometry sizes (e.g., put
 * as many directory entries per sector as possible).
 *
 * Since I'll want to run this on an MS-DOS machine, I am careful to
 * allocate memory in small chunks, use an ANSI C compiler and use
 * longs when the numbers will get bigger than 16 bits (i.e., the
 * number of bytes per disk).
 *
 *
 * Author: version 1.0.
 *	George Phillips <phillips@cs.ubc.ca>
 *	Department of Computer Science
 *	University of British Columbia
 *
 * New for version 1.1.
 *
 * 1) Added a help message listing the commands.
 * 2) Added the ability to load different file images from within afid,
 *    so you no longer need to exit and enter with a new image.
 * 3) Prompts the user when attempting to exit without saving a changed
 *    disk image.
 * 4) Added new commands like load, type, lock, unlock, rename, delete, list
 *
 * Author: version 1.1.
 * 	Peter Nyhlen <pnyhlen@usa.net>
 */

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

/* This stuff really belongs in a header file */
/******************* afid.h *******************/
struct track
{
    unsigned char** sector;
};

struct disk
{
    int			bytes_per_sector;
    int			sectors_per_track;
    int			tracks_per_disk;
    struct track*	track;
};

struct afile
{
    int filetype;
    struct disk* d;
    int ts_s, ts_t, ts_ent, dt_t, dt_s, dt_pos;
    int eof, secgroup, dir_t, dir_s, dir_off;
};

/* handy disk address macros for getting at particular bytes and words */
#define byteat(d, t, s, n) ((d)->track[(t)].sector[(s)][(n)])
#define wordat(d, t, s, n) (byteat(d, t, s, n) + byteat(d, t, s, (n) + 1) * 256)

/* logical constants */
#define DELETE   'D'
#define UNDELETE 'U'
#ifndef TRUE
#define TRUE 1
#endif
#ifndef FALSE
#define FALSE 0
#endif
/*************** end of afid.h ****************/


/* Function Prototypes */
void build_disk_mem(struct disk*);
void load_disk(struct disk* d, char*);
void read_disk(struct disk*, FILE*);
void verify_geometry(struct disk*);

void list_help_msg();
int confirm_changes(int);

void catalog(struct disk*);
int  apptoascii(int), asciitoapp(int);
void* alloc(int);

void show_freemap(struct disk*);
int  isfree(struct disk*, int, int);
int  allocate_sector(struct disk*, int*, int*, int);
void alloc_sector(struct disk*, int, int);
void free_sector(struct disk*, int, int);
void zero_sector(struct disk*, int, int);

int aopen(struct disk*, char*, struct afile*, char*);
int aclose(struct afile*);
int agetc(struct afile*);
int aputc(struct afile*, int);
long agetw(struct afile*);

int isfilename(char*, struct disk*, int, int, int);
void aputfilename(char*, struct disk*, int, int, int);
int tokenize(char* , char** );
void hexdump(FILE*, struct afile*);
int traverse_tsl(struct afile* af, int operation);
int dtraverse_tsl(struct afile* af, int operation);
int utraverse_tsl(struct afile* af, int operation);


main(int argc, char* argv[])
{
    struct disk d;
    struct afile af;
    int textmode = 0;
    int disk_image_loaded = TRUE;
    int any_unsaved_changes = FALSE;

    /* Initialize Disk Structure */
    d.bytes_per_sector = 256;
    d.sectors_per_track = 16;
    d.tracks_per_disk = 35;
    build_disk_mem(&d);

    /* Process Arguments */
    switch (argc)
    {
	case 1:
	    disk_image_loaded = FALSE;
	    list_help_msg();
	    break;
	case 2:
	    load_disk(&d, argv[1]);
	    break;
	default:
	    fprintf(stderr, "usage: afid disk-image-file\n");
	    exit(1);
    }

    /* Loop Through Interactive Command Processing */
    for (;;)
    {
	char buf[1024], *arg[10], *p, *cmd;

	/* Command Line Prompt */
	printf("] ");
	fflush(stdout);

	/* Get User Input */
	if (!fgets(buf, 1024, stdin))
	    break;

	/* Parse Command Line */
	tokenize(buf, arg);
	cmd = arg[0];
	if (!*cmd)
	    continue;

	/* Process Command */
	if (!strcmp(cmd, "load"))
	{
	    if (confirm_changes(any_unsaved_changes))
	    {
		load_disk(&d, arg[1]);
		disk_image_loaded = TRUE;
	    }
	}
	else if (!strcmp(cmd, "help"))
	    list_help_msg();
	else if (!strcmp(cmd, "quit") || !strcmp(cmd, "exit"))
	{
	    if (confirm_changes(any_unsaved_changes))
		break;
	}
	else if (!disk_image_loaded)
	    fprintf(stderr, "no disk image loaded.\nunable to execute %s\n", cmd);
	else if (!strcmp(cmd, "catalog"))
	    catalog(&d);
	else if (!strcmp(cmd, "free"))
	    show_freemap(&d);
	else if (!strcmp(cmd, "text"))
	    textmode = 1;
	else if  (!strcmp(cmd, "binary"))
	    textmode = 0;
	else if  (!strcmp(cmd, "mode"))
	    switch (textmode)	/* display transfere mode */
	    {
		case 0:
		    printf("binary\n");
		    break;
		case 1:
		    printf("text\n");
		    break;
		default:
		    fprintf(stderr, "corrupted file type.\n");
		    exit(2);
	    }
	else if  (!strcmp(cmd, "type"))
	{
	    int   filetype;
	    /* check file type specified */
	    switch (toupper(arg[2][0]))
	    {
		case 'T': filetype = 0; break;
		case 'I': filetype = 1; break;
		case 'A': filetype = 2; break;
		case 'B': filetype = 4; break;
		case 'S': filetype = 8; break;
		case 'R': filetype = 16; break;
		default : filetype = -1; break;
	    }

	    /* define file type */
	    if (filetype<0)
		fprintf(stderr, "filetype %s is invalid.\n", arg[2]);
	    else if (!aopen(&d, arg[1], &af, textmode ? "rt" : "rb"))
		fprintf(stderr, "couldn't open ][ file %s\n", arg[1]);
	    else
	    {
		/* check for locked file */
		if (byteat(af.d, af.dir_t, af.dir_s, af.dir_off+2) & 0x80)
		    fprintf(stderr, "%s is locked.\n", arg[1]);
		else /* find file and change type */
		{
		    byteat(af.d, af.dir_t, af.dir_s, af.dir_off+2) = filetype;
		    any_unsaved_changes = TRUE;
		}

		if (aclose(&af) == EOF)
		    fprintf(stderr, "error on closing ][ file %s\n", arg[1]);
	    }
	}
	else if (!strcmp(cmd, "reorder"))
	{
	    int t, s;
	    void* temp;

	    for (t = 0; t < d.tracks_per_disk; t++)
	    {
		for (s = 1; s < d.sectors_per_track / 2; s++)
		{
		    temp = d.track[t].sector[s];
		    d.track[t].sector[s] =
			    d.track[t].sector[d.sectors_per_track - s - 1];
		    d.track[t].sector[d.sectors_per_track - s - 1] = temp;
		}
	    }
	    any_unsaved_changes = TRUE;
	}
	else if (!strcmp(cmd, "lock") || !strcmp(cmd, "unlock"))
	{
	    /* find file and lock/unlock it */
	    if (!aopen(&d, arg[1], &af, textmode ? "rt" : "rb"))
		fprintf(stderr, "couldn't find ][ file %s\n", arg[1]);
	    else
	    {
		if (!strcmp(cmd, "lock"))
		    byteat(af.d, af.dir_t, af.dir_s, af.dir_off+2) |= 0x80;
		else
		    byteat(af.d, af.dir_t, af.dir_s, af.dir_off+2) &= 0x7F;
		any_unsaved_changes = TRUE;
	    }

	    if (aclose(&af) == EOF)
		fprintf(stderr, "error on closing ][ file %s\n", arg[1]);
	}
	else if (!strcmp(cmd, "rename"))
	{
	    /* check for existing file name */
	    if (aopen(&d, arg[2], &af, textmode ? "rt" : "rb"))
	    {
		fprintf(stderr, "couldn't rename file %s\n", arg[2]);
		fprintf(stderr, "%s already exists!\n", arg[2]);
		if (aclose(&af) == EOF)
		    fprintf(stderr, "error on closing ][ file %s\n", arg[2]);
	    }
	    else if (!aopen(&d, arg[1], &af, textmode ? "rt" : "rb"))
		fprintf(stderr, "couldn't open ][ file %s\n", arg[1]);
	    else
	    {
	        /* check for locked file */
		if (byteat(af.d, af.dir_t, af.dir_s, af.dir_off+2) & 0x80)
		    fprintf(stderr, "%s is locked.\n", arg[1]);
		else /* rename file */
		{
		    aputfilename(arg[2], af.d, af.dir_t, af.dir_s, af.dir_off);
		    any_unsaved_changes = TRUE;
		}

		if (aclose(&af) == EOF)
		    fprintf(stderr, "error on closing ][ file %s\n", arg[1]);
	    }
	}
	else if (!strcmp(cmd, "read") || !strcmp(cmd, "list"))
	{
	    FILE* fp;

	    if (*cmd == 'r')
		fp = fopen(arg[1], textmode ? "w" : "wb");
	    else
		fp = stdout;

	    if (!aopen(&d, arg[1], &af, textmode ? "rt" : "rb"))
		fprintf(stderr, "couldn't open ][ file %s\n", arg[1]);
	    else
	    {
		int n, ch;
		long start, len;

		if (!textmode)
		{
		    printf("%s is a type %c file\n", arg[1], af.filetype);
		    start = agetw(&af);
		    len = agetw(&af);
		    if (fp)
		    {
			fputc(start & 255, fp);
			fputc((start >> 8) & 255, fp);
			fputc(len & 255, fp);
			fputc((len >> 8) & 255, fp);
		    }
		    printf("it is %ld bytes long (starts at %d)\n", len, start);
		    n = 4;
		    while ((ch = agetc(&af)) != EOF)
		    {
			if (fp)
			    fputc(ch, fp);
			n++;
		    }
		    printf("or is that %d bytes long\n", n);
		}
		else	/* Print text or hexdump */
		{
		    if (byteat(af.d, af.dir_t, af.dir_s, af.dir_off+2) & 0x7f)
			hexdump(fp, &af);
		    else
			while ((ch = agetc(&af)) != EOF)
			{
			    if (ch == '\0')
				break;
			    ch = apptoascii(ch);
			    if (ch == '\r')
				ch = '\n';
			    if (fp)
				fputc(ch, fp);
			}
		}
		if (fp && *cmd == 'r')
		    fclose(fp);
		if (aclose(&af) == EOF)
		    fprintf(stderr, "error on closing ][ file %s\n", arg[1]);
	    }
	}
	else if (!strcmp(cmd, "write"))
	{
	    FILE* fp;

	    if (!(fp = fopen(arg[1], textmode ? "r" : "rb")))
		fprintf(stderr, "couldn't open unix file %s\n", arg[1]);
	    else
	    {
		if (!aopen(&d, arg[1], &af, textmode ? "wt" : "wb"))
		{
		    fprintf(stderr, "couldn't open ][ file %s\n", arg[1]);
		    fclose(fp);
		}
		else
		{
		    int ch;

		    if (!textmode)
		    {
			while ((ch = fgetc(fp)) != EOF)
			{
			    if (aputc(&af, ch) == EOF)
			    {
				fprintf(stderr, "error on write to ][ file %s\n", arg[1]);
				break;
			    }
			}
		    }
		    else
		    {
			while ((ch = fgetc(fp)) != EOF)
			{
			    if (ch == '\n')
				ch = '\r';
			    ch = asciitoapp(ch);
			    if (aputc(&af, ch) == EOF)
			    {
				fprintf(stderr, "error on write to ][ file %s\n", arg[1]);
				break;
			    }
			}
			/* HACK! :-( check that error code! */
			aputc(&af, '\0');
		    }
		    any_unsaved_changes = TRUE;

		    if (aclose(&af) == EOF)
			    fprintf(stderr, "error on closing ][ file %s\n", arg[1]);
		}
	    }
	}
	else if (!strcmp(cmd, "delete"))
	{
	    if (!aopen(&d, arg[1], &af, textmode ? "rt" : "rb"))
		fprintf(stderr, "couldn't open ][ file %s\n", arg[1]);
	    else
	    {
		/* check for locked file */
		if (byteat(af.d, af.dir_t, af.dir_s, af.dir_off+2) & 0x80)
		    fprintf(stderr, "%s is locked.\n", arg[1]);
		else /* delete file */
		{
		    /* save off tsl track at last name position */
		    byteat(af.d, af.dir_t, af.dir_s, af.dir_off+0x20) = af.ts_t;

		    /* set tsl track to FF hex */
		    byteat(af.d, af.dir_t, af.dir_s, af.dir_off) = 0xff;

		    traverse_tsl(&af, DELETE);
		    any_unsaved_changes = TRUE;
		}

		if (aclose(&af) == EOF)
		    fprintf(stderr, "error on closing ][ file %s\n", arg[1]);
	    }
	}
	else if (!strcmp(cmd, "undelete"))
	{
	    int count;

	    /* clean up file name */
	    for (p = arg[1]; *p; p++)
		*p = toupper(*p);

	    /* find directory entry */
	    if (!aopen(&d, arg[1], &af, "ub"))
		fprintf(stderr, "couldn't find deleted ][ file %s\n", arg[1]);
	    else
	    {
		/* see if tsl is still there */
		/*
		count = utraverse_tsl(&af, UNDELETE);
		*/
		if (isfree(af.d, af.ts_t, af.ts_s))
		{
		    alloc_sector(af.d, af.ts_t, af.ts_s);
		    count = 1 + traverse_tsl(&af, UNDELETE);
		}
		else
		{
		    byteat(af.d, af.dir_t, af.dir_s, af.dir_off) = 0;
		    byteat(af.d, af.dir_t, af.dir_s, af.dir_off+1) = 0;
		}

		if (byteat(af.d, af.dir_t, af.dir_s, af.dir_off+0x21) != count)
		{
		    fprintf(stderr, "couldn't restore complete ][ file %s\n", arg[1]);
		    fprintf(stderr, "sectors have been allocated to other files\n");
		    fprintf(stderr, "truncated version of ][ file %s has been created\n", arg[1]);
		    byteat(af.d, af.dir_t, af.dir_s, af.dir_off+0x21) = count;
		}

		if (aclose(&af) == EOF)
		    fprintf(stderr, "error on closing ][ file %s\n", arg[1]);
	    }
	    any_unsaved_changes = TRUE;
	}
	else if (!strcmp(cmd, "save"))
	{
	    FILE* fp;
	    int t, s;

	    if (!(fp = fopen(arg[1], "wb")))
		fprintf(stderr, "can't write to unix disk image %s\n", arg[1]);
	    else
	    {
		for (t = 0; t < d.tracks_per_disk; t++)
		{
		    for (s = 0; s < d.sectors_per_track; s++)
		    {
			if (fwrite(d.track[t].sector[s], d.bytes_per_sector, 1, fp) != 1)
			{
			    fprintf(stderr,
				    "error while writing unix disk image %s\n", arg[1]);
			    fclose(fp);
			    fp = 0;
			    t = d.tracks_per_disk;
			    break;
			}
		    }
		}
		any_unsaved_changes = FALSE;
		if (fp && fclose(fp) == EOF)
		    fprintf(stderr, "error while writing unix disk image %s\n", arg[1]);
	    }
	}
	else
	    fprintf(stderr, "command %s not understood\n", cmd);
    }

    exit(0);
}


/* allocate memory for the disk structure based on the geometry fields */
void build_disk_mem(struct disk* d)
{
    int	t, s;
    unsigned char* secmem;

    d->track = alloc(sizeof(struct track) * d->tracks_per_disk);
    for (t = 0; t < d->tracks_per_disk; t++)
    {
	d->track[t].sector = alloc(sizeof(unsigned char*) * d->sectors_per_track);
	secmem = alloc(d->bytes_per_sector * d->sectors_per_track);
	for (s = 0; s < d->sectors_per_track; s++)
	    d->track[t].sector[s] =  secmem + s * d->bytes_per_sector;
    }
}


/* load a disk image into memory */
void load_disk(struct disk* d, char* disk_image)
{
    FILE* fp;
    if (!(fp = fopen((char *)disk_image, "rb")))
    {
	perror("fopen");
	fprintf(stderr, "couldn't open %s\n", disk_image);
	exit(1);
    }

    read_disk(d, fp);
    verify_geometry(d);
    fclose(fp);
}


/* slurp all the sectors from the disk image into memory */
void read_disk(struct disk* d, FILE* fp)
{
    int t,s;
    char c;

    for (t=0; t < d->tracks_per_disk; t++)
	for (s=0; s < d->sectors_per_track; s++)
	    if (fread(d->track[t].sector[s], d->bytes_per_sector, 1, fp) != 1)
	    {
		fprintf(stderr, "EOF while reading disk!\n");
		exit(1);
	    }

    if (fread(&c, 1, 1, fp) == 1)
	    fprintf(stderr, "warning: extra data in disk-image\n");
}


/* do a sanity check between our pre-defined geometry and the geometry
 * information in the volume table of contents (VTOC, track 0x11, sector 0).
 * Print the DOS version number of the disk, just for fun.
 */
void verify_geometry(struct disk* d)
{
    int tracks_per_disk, sectors_per_track, bytes_per_sector;

    tracks_per_disk = byteat(d, 0x11, 0, 0x34);
    sectors_per_track = byteat(d, 0x11, 0, 0x35);
    bytes_per_sector = wordat(d, 0x11, 0, 0x36);

    printf("DOS release number: %d\t", byteat(d, 0x11, 0, 3));
    printf("Volume number: %d\n", byteat(d, 0x11, 0, 6));

    if (tracks_per_disk != d->tracks_per_disk ||
	sectors_per_track != d->sectors_per_track ||
	bytes_per_sector != d->bytes_per_sector)
    {
	fprintf(stderr,
	"afid: warning: initial disk geometry isn't the same as the disk geometry\n");
	fprintf(stderr, "information that is stored on the disk.\n");
	fprintf(stderr, "initial: %d tracks, %d sectors, %d bytes per sector\n",
		d->tracks_per_disk, d->sectors_per_track, d->bytes_per_sector);
	fprintf(stderr, "on disk: %d tracks, %d sectors, %d bytes per sector\n",
		tracks_per_disk, sectors_per_track, bytes_per_sector);
    }
}


/* display the help message */
void list_help_msg()
{
    char *help_msg[] = {
    "\n\tafid  version 1.1\n",
    "load image   - load a disk image into memory",
    "catalog      - show a list of files on the disk image",
    "free         - map and number of free sectors",
    "quit, exit   - leave afid",
    "binary       - (default) no translation of data during read/write",
    "text         - CR <-> LF & Apple <-> ASCII translation during read/write",
    "mode         - lists text/binary transfere mode for read/write/list",
    "type file,X  - change the file type.  X = T, I, A, B, S, or R.",
    "reorder      - switch disk sector order between DOS 3.3 (default) and ProDOS",
    "list file    - copy the given file from the disk image to the screen",
    "read file    - copy the given file from the disk image to a UNIX file",
    "write file   - copy the given UNIX file onto the disk image",
    "save image   - save the in-memory disk image to the given UNIX file",
    "lock file    - write protect a file on the disk image",
    "unlock file  - unwrite protect a file on the disk image",
    "rename f1,f2 - rename file f1 to f2 on the disk image",
    "delete file  - delete a file from the disk image.",
    "undelete fil - undelete a file from the disk image, if possible.",
    ""};
    int i;

    for (i=0; help_msg[i][0]; i++)
	printf("%s\n", help_msg[i]);
}


/* check and confirm any data loss*/
int confirm_changes(int any_unsaved_changes)
{
    int answer = TRUE;

    if (any_unsaved_changes)
    {
	printf("you have unsaved changes on your disk Image.\n");
	printf("shall we continue and lose your changes? ");
	answer = getchar();
	if (answer != 'y' && answer != 'Y')
	    answer = FALSE;

	/* clear remaining input*/
	if (answer != '\n')
	    while (getchar() != '\n')
		;
    }
    
    return(answer);
}


/* print out what sectors have been allocated */
void show_freemap(struct disk* d)
{
    int s, t, tens, prev, total, allocated;

    printf("Sectors     Tracks 0 -> %d, * = sector allocated:\n",
	    d->tracks_per_disk);
    printf("   ");
    for (t = 0, prev = 0; t < d->tracks_per_disk; t++)
    {
	tens = (t / 10) % 10;
	printf("%c", tens == prev ? ' ' : '0' + tens);
	prev = tens;
    }
    printf("\n   ");
    for (t = 0; t < d->tracks_per_disk; t++)
	printf("%d", t % 10);
    printf("\n");

    total = d->tracks_per_disk * d->sectors_per_track;
    allocated = 0;
    for (s = 0; s < d->sectors_per_track; s++)
    {
	printf("%2d ", s);
	for (t = 0; t < d->tracks_per_disk; t++)
	{
	    if (isfree(d, t, s))
		putchar(' ');
	    else
	    {
		putchar('*');
		allocated++;
	    }
	}
	printf("\n");
    }
    printf("%d total sectors, %d allocated, %d free\n",
	    total, allocated, total - allocated);
    
    printf("sector mask: 0x%2.2x 0x%2.2x 0x%2.2x 0x%2.2x %s\n",
	    byteat(d, 0x11, 0, 0x30),
	    byteat(d, 0x11, 0, 0x31),
	    byteat(d, 0x11, 0, 0x32),
	    byteat(d, 0x11, 0, 0x33),
	    "is this the reasonable 0xffff0000?");
}


/* display the directory of files on the disk */
void catalog(struct disk* d)
{
    int t, s, nt, ns, dir, filetype, i, eod;

    t = 0x11;
    s = 0;
    printf("disk volume %d\n", byteat(d, t, s, 6));
    while ((nt = byteat(d, t, s, 1)) && (ns = byteat(d, t, s, 2)))
    {
	t = nt;
	s = ns;
	eod = FALSE;
	for (dir = 0xb; dir + 35 <= d->bytes_per_sector; dir += 35)
	{
	    if (byteat(d, t, s, dir) == 0xff)
		continue;
	    if (byteat(d, t, s, dir) == 0)
	    {
		eod = TRUE;
		break;
	    }
	    filetype = byteat(d, t, s, dir + 2);
	    putchar((filetype & 0x80) ? '*' : ' ');
	    switch (filetype & 0x7f)
	    {
		case 0: putchar('T'); break;
		case 1: putchar('I'); break;
		case 2: putchar('A'); break;
		case 4: putchar('B'); break;
		case 8: putchar('S'); break;
		case 16: putchar('R'); break;
		default: putchar('?'); break;
	    }
	    printf(" %03d ", wordat(d, t, s, dir + 0x21));
	    for (i = 3; i < 0x20; i++)
		putchar(apptoascii(byteat(d, t, s, dir + i)));
	    putchar('\n');
	}
	if (eod)
	    break;
    }
}


/* aopen -- open an Apple ][ file (for reading only), return !0 if successful */
/* would be nice to share directory reading stuff with catalog() */
/* I assume that I cannot extend the directory and that the entire directory
 * has been pre-allocated and pre-initialized by Apple DOS.  I also try
 * to initialize things in the same way that Apple DOS would (e.g., every
 * file will contain at least 2 sectors).
 */
int aopen(struct disk* d, char* filename, struct afile* af, char* mode)
{
    int t, s, nt, ns, dir, eod, found;
    int firstfree, fft, ffs, ffoff;
    char* p;

    t = 0x11;
    s = 0;
    found = firstfree = FALSE;

    /* ensure uppercase file name */
    for (p = filename; *p; p++)
	*p = toupper(*p);

    while ((nt = byteat(d, t, s, 1)) && (ns = byteat(d, t, s, 2)))
    {
	t = nt;
	s = ns;
	eod = FALSE;
	for (dir = 0xb; dir + 35 <= d->bytes_per_sector; dir += 35)
	{
	    /* a formerly deleted file */
	    if (byteat(d, t, s, dir) == 0xff)
	    {
		if (!firstfree)
		{
		    firstfree = TRUE;
		    fft = t;
		    ffs = s;
		    ffoff = dir;
		}
		if ((*mode == 'u') && (found = isfilename(filename, d, t, s, dir)))
		    break;
		else
		    continue;
	    }
	    /* end of dir list */
	    if (byteat(d, t, s, dir) == 0)
	    {
		if (!firstfree)
		{
		    firstfree = TRUE;
		    fft = t;
		    ffs = s;
		    ffoff = dir;
		}
		eod = TRUE;
		break;
	    }

	    /* check filename of an existing file in the directory */
	    if ((*mode != 'u') && (found = isfilename(filename, d, t, s, dir)))
		break;
	}
	if (eod || found)
	    break;
    }

    if (!found)
    {
	int tt, ts, fs, ft;

	if (*mode != 'w' || !firstfree)
	    return 0;
	if (!allocate_sector(d, &tt, &ts, -1))
	    return 0;
	if (!allocate_sector(d, &ft, &fs, t))
	{
	    free_sector(d, tt, ts);
	    return 0;
	}
	zero_sector(d, tt, ts);
	zero_sector(d, ft, fs);

	/* fill in directory entry */
	byteat(d, fft, ffs, ffoff) = tt;
	byteat(d, fft, ffs, ffoff + 1) = ts;
	byteat(d, fft, ffs, ffoff + 2) = mode[1] == 'b' ? 4 : 0;
	
	aputfilename(filename, d, fft, ffs, ffoff);

	byteat(d, fft, ffs, ffoff + 0x21) = 2;
	byteat(d, fft, ffs, ffoff + 0x22) = 0;

	/* fill in first track + sector list sector */
	byteat(d, tt, ts, 1) = 0;
	byteat(d, tt, ts, 2) = 0;
	byteat(d, tt, ts, 5) = 0;
	byteat(d, tt, ts, 6) = 0;
	byteat(d, tt, ts, 0xc) = ft;
	byteat(d, tt, ts, 0xd) = fs;

	/* fudge some values so the fallthough will be right */
	t = fft;
	s = ffs;
	dir = ffoff;
    }

    /* enforce write protection on files */
    if (*mode == 'w' && (byteat(d, t, s, dir +2) & 0x80))
	return 0;

    switch (byteat(d, t, s, dir + 2) & 0x7f)
    {
	case 0: af->filetype = 'T'; break;
	case 1: af->filetype = 'I'; break;
	case 2: af->filetype = 'A'; break;
	case 4: af->filetype = 'B'; break;
	case 8: af->filetype = 'S'; break;
	case 16: af->filetype = 'R'; break;
	default: af->filetype = '?'; break;
    }

    /* note which disk this file is on */
    af->d = d;

    /* restore deleted file for undelete */
    if (*mode == 'u')
    {
	/* restore tsl track from last name position */
	byteat(d, t, s, dir) = byteat(d, t, s, dir+0x20);
	byteat(d, t, s, dir+0x20) = asciitoapp(' ');
    }

    /* keep track of where we are in the track/sector list */
    af->ts_t = byteat(d, t, s, dir);
    af->ts_s = byteat(d, t, s, dir + 1);

    /* initialize so that first read initializes other stuff */
    af->ts_ent = -1;
    af->dt_pos = d->bytes_per_sector;

    /* these variables are only used by the writing routines */
    af->eof = FALSE;
    af->secgroup = 0;
    af->dir_t = t;
    af->dir_s = s;
    af->dir_off = dir;

    return 1;
}


/* close the apple ][ file */
/* returns 0 on success, EOF on failure */
/* For now, there is nothing to do, but later versions may require this
 * in order to fix up the disk (i.e., because of sector pre-allocation).
 * Also, other versions may wish to truncate the file!
 */
int aclose(struct afile* af)
{
    return 0;
}


/* get a character from the open apple ][ file */
/* return the character or EOF on end of file */
int agetc(struct afile* af)
{
    int	t, s;

    if (af->ts_t == 0 && af->ts_s == 0)
	return EOF;

    if (af->dt_pos >= af->d->bytes_per_sector)
    {
	af->dt_pos = 0;
	if (++af->ts_ent >= byteat(af->d, 0x11, 0, 0x27))
	{
	    af->ts_ent = 0;
	    t = byteat(af->d, af->ts_t, af->ts_s, 1);
	    s = byteat(af->d, af->ts_t, af->ts_s, 2);
	    af->ts_t = t;
	    af->ts_s = s;
	    if (t == 0 && s == 0)
		return EOF;
	}
	af->dt_t = byteat(af->d, af->ts_t, af->ts_s, 12 + af->ts_ent * 2);
	af->dt_s = byteat(af->d, af->ts_t, af->ts_s, 12 + af->ts_ent * 2 + 1);
    }

    if (af->dt_t == 0 && af->dt_s == 0)
	return EOF;
    
    return ((int)byteat(af->d, af->dt_t, af->dt_s, af->dt_pos++));
}


/* put a character into the open apple ][ file */
/* returns either the character written or EOF if some error occured */
/* I don't keep enough information around to give really good hints
 * about where to allocate the next sector.  No big deal, really.
 */
int aputc(struct afile* af, int ch)
{
    int t, s;

    if (af->eof)
	return EOF;

    if (af->dt_pos >= af->d->bytes_per_sector)
    {
	af->dt_pos = 0;
	if (++af->ts_ent >= byteat(af->d, 0x11, 0, 0x27))
	{
	    af->ts_ent = 0;
	    t = byteat(af->d, af->ts_t, af->ts_s, 1);
	    s = byteat(af->d, af->ts_t, af->ts_s, 2);
	    af->secgroup += byteat(af->d, 0x11, 0, 0x27);
	    if (t == 0 && s == 0)
	    {
		if (!allocate_sector(af->d, &t, &s, af->ts_t))
		{
		    af->eof = TRUE;
		    return EOF;
		}
		byteat(af->d, af->dir_t, af->dir_s, af->dir_off + 0x21)++;
		byteat(af->d, af->ts_t, af->ts_s, 1) = t;
		byteat(af->d, af->ts_t, af->ts_s, 2) = s;
		zero_sector(af->d, t, s);
		byteat(af->d, t, s, 5) = af->secgroup & 255;
		byteat(af->d, t, s, 6) = (af->secgroup >> 8) & 255;
	    }
	    af->ts_t = t;
	    af->ts_s = s;
	}
	af->dt_t = byteat(af->d, af->ts_t, af->ts_s, 12 + af->ts_ent * 2);
	af->dt_s = byteat(af->d, af->ts_t, af->ts_s, 12 + af->ts_ent * 2 + 1);
    }

    if (af->dt_t == 0 && af->dt_s == 0)
    {
	if (!allocate_sector(af->d, &af->dt_t, &af->dt_s, af->ts_t))
	{
	    af->eof = TRUE;
	    return EOF;
	}
	zero_sector(af->d, af->dt_t, af->dt_s);
	byteat(af->d, af->dir_t, af->dir_s, af->dir_off + 0x21)++;
	byteat(af->d, af->ts_t, af->ts_s, 12 + af->ts_ent * 2) = af->dt_t;
	byteat(af->d, af->ts_t, af->ts_s, 12 + af->ts_ent * 2 + 1) = af->dt_s;
    }

    byteat(af->d, af->dt_t, af->dt_s, af->dt_pos++) = ch;
    return ch;
}


/* get a 2 byte word (little endian, of course) from an apple ][ file */
long agetw(struct afile* af)
{
    int b1, b2;

    if ((b1 = agetc(af)) == EOF || (b2 = agetc(af)) == EOF)
	return EOF;
    
    return b1 + b2 * 256;
}


/* take an Apple ][ character code and convert it to ASCII */
/* the current implementation is workable but NOT right */
int apptoascii(int c)
{
    return c & 0x7f;
}


/* take an ASCII character code and convert it to Apple ][ code */
/* like apptoascii(), this is just a workable but NOT right implementation */
int asciitoapp(int c)
{
    return c | 0x80;
}


/* sector allocation + de-allocation routines */

/* return non-zero if the given sector has not been allocated, 0 if free */
/* this routine is quite dependent on 16 sectors per track */
int isfree(struct disk* d, int t, int s)
{
    return
	    byteat(d, 0x11, 0, 0x38 + t * 4 + (s < 8)) &	/* free map */
	    (1 << (s & 7));				/* right bit in map */

/*
    This gives the sector mask which should be ANDed into the above,
    but the sector masks on disks I've seen seem bogus (they should be 0xff)
    but typically have only 1 or 2 bits set(!)

    byteat(d, 0x11, 0, 0x30 + (s < 8))
*/
}


/* allocate a sector for use.  If pref_track is valid, the sector will
 * be allocated from the given track, if possible.  If pref_track is
 * invalid (e.g., < 0), then a search is made for an empty track and
 * if none is found then we just look for the first sector we can find.
 *
 * As with the other sector routines, 16 sectors per track is hard-coded.
 */
int allocate_sector(struct disk* d, int* t, int* s, int pref_track)
{
    int	i, j, found_track;

    if (pref_track < 0 || pref_track >= d->tracks_per_disk)
    {
	found_track = 0;
	for (i = 0; i < d->tracks_per_disk; i++)
	{
	    if (byteat(d, 0x11, 0, 0x38 + i * 4) == 0xff &&
		    byteat(d, 0x11, 0, 0x38 + i * 4 + 1) == 0xff)
	    {
		found_track = 1;
		break;
	    }
	}
	if (found_track)
	{
	    alloc_sector(d, *t = i, *s = 15);
	    return 1;
	}
    }
    else
	for (i = d->sectors_per_track - 1; i >= 0; i--)
	    if (isfree(d, pref_track, i))
	    {
		alloc_sector(d, *t = pref_track, *s = i);
		return 1;
	    }

    for (i = 0; i < d->tracks_per_disk; i++)
	for (j = d->sectors_per_track - 1; j >= 0; j--)
	    if (isfree(d, i, j))
	    {
		alloc_sector(d, *t = i, *s = j);
		return 1;
	    }
    return 0;
}


/* mark the given sector as in use */
void alloc_sector(struct disk* d, int t, int s)
{
    byteat(d, 0x11, 0, 0x38 + t * 4 + (s < 8)) &= ~ (1 << (s & 7));
}


/* mark the given sector as free */
void free_sector(struct disk* d, int t, int s)
{
    byteat(d, 0x11, 0, 0x38 + t * 4 + (s < 8)) |= 1 << (s & 7);
}


/* clear an entire sector to zeros */
void zero_sector(struct disk* d, int t, int s)
{
    int i;

    for (i = 0; i < d->bytes_per_sector; i++)
	byteat(d, t, s, i) = 0;
}


/* allocate memory to structures */
void* alloc(int n)
{
    void* p;

    if (!(p = malloc(n)))
    {
	fprintf(stderr, "afid: out of memory\n");
	exit(1);
    }
    return p;
}


/* return TRUE if directory entry matches requested file name */
int isfilename(char* filename, struct disk* d, int t, int s, int b)
{
    int i;
    char* p;

    /* check file name */
    for (p = filename, i = 3; *p && i < 0x20; i++, p++)
	if (*p != apptoascii(byteat(d, t, s, b + i)))
	    return (FALSE);

    /* check remainder of name space */
    for (; i < 0x20; i++)
	if (apptoascii(byteat(d, t, s, b + i)) != ' ')
	    return (FALSE);

    return (TRUE);
}


/* write file name in the directory */
void aputfilename(char* filename, struct disk* d, int t, int s, int b)
{
    int i;
    char* p;

    /* write filename */
    for (i = 3, p = filename; *p && i <= 0x20; i++, p++)
	byteat(d, t, s, b + i) = asciitoapp(*p);

    /* fill remained of name space */
    for (; i <= 0x20; i++)
	byteat(d, t, s, b + i) = asciitoapp(' ');
}


/* Parse Command Line and break it up into tokens */
int tokenize(char* buf, char** token)
{
    int   count = -1;
    char* p = buf;

    /* tokenize arguments */
    while(*p)
    {
	/* clear leading spaces */
	for (; *p && isspace(*p); p++)
	    ;

	/* tokenize argument */
	token[++count] = p;
	for (; *p && *p != '\n' && *p != (count ? ',':' '); p++)
	    ;

	/* terminate token */
	if (*p)
	    *p++ = '\0';
    }
    return (count);
}


/* Generate a hex dump of an apple ][ file */
void hexdump(FILE* fp, struct afile* af)
{
    char achar[] = {
    '@',' ','a',' ','b',' ','c',' ','d',' ','e',' ','f',' ','g',' ',
    'h',' ','i',' ','j',' ','k',' ','l',' ','m',' ','n',' ','o',' ',
    'p',' ','q',' ','r',' ','s',' ','t',' ','u',' ','v',' ','w',' ',
    'x',' ','y',' ','z',' ','~','~','~','~','~','~','~','~','~','~',
    ' ',' ','!',' ','"',' ','#',' ','$',' ','%',' ','&',' ','\'',' ',
    '(',' ',')',' ','*',' ','+',' ',',',' ','-',' ','.',' ','/',' ',
    '0',' ','1',' ','2',' ','3',' ','4',' ','5',' ','6',' ','7',' ',
    '8',' ','9',' ',':',' ',';',' ','<',' ','=',' ','>',' ','?',' ',
    '@',' ','a',' ','b',' ','c',' ','d',' ','e',' ','f',' ','g',' ',
    'h',' ','i',' ','j',' ','k',' ','l',' ','m',' ','n',' ','o',' ',
    'p',' ','q',' ','r',' ','s',' ','t',' ','u',' ','v',' ','w',' ',
    'x',' ','y',' ','z',' ','~','~','~','~','~','~','~','~','~','~',
    ' ',' ','!',' ','"',' ','#',' ','$',' ','%',' ','&',' ','\'',' ',
    '(',' ',')',' ','*',' ','+',' ',',',' ','-',' ','.',' ','/',' ',
    '0',' ','1',' ','2',' ','3',' ','4',' ','5',' ','6',' ','7',' ',
    '8',' ','9',' ',':',' ',';',' ','<',' ','=',' ','>',' ','?',' ',
    '@',' ','^','A','^','B','^','C','^','D','^','E','^','F','^','G',
    '\\','b','^','I','^','J','^','K','^','L','\\','n','^','N','^','O',
    '^','P','^','Q','^','R','^','S','^','T','\\','>','^','V','^','W',
    '^','X','^','Y','^','Z','\\','[','\\','\\','\\',']','\\','^','\\','_',
    ' ',' ','!',' ','"',' ','#',' ','$',' ','%',' ','&',' ','\'',' ',
    '(',' ',')',' ','*',' ','+',' ',',',' ','-',' ','.',' ','/',' ',
    '0',' ','1',' ','2',' ','3',' ','4',' ','5',' ','6',' ','7',' ',
    '8',' ','9',' ',':',' ',';',' ','<',' ','=',' ','>',' ','?',' ',
    '@',' ','A',' ','B',' ','C',' ','D',' ','E',' ','F',' ','G',' ',
    'H',' ','I',' ','J',' ','K',' ','L',' ','M',' ','N',' ','O',' ',
    'P',' ','Q',' ','R',' ','S',' ','T',' ','U',' ','V',' ','W',' ',
    'X',' ','Y',' ','Z',' ','[',' ','\\',' ',']',' ','^',' ','_',' ',
    '~','~','a',' ','b',' ','c',' ','d',' ','e',' ','f',' ','g',' ',
    'h',' ','i',' ','j',' ','k',' ','l',' ','m',' ','n',' ','o',' ',
    'p',' ','q',' ','r',' ','s',' ','t',' ','u',' ','v',' ','w',' ',
    'x',' ','y',' ','z',' ','~','~','~','~','~','~','~','~','~','~'};
    char str1[25], str2[17];
    long adrs;
    int ch;

    str1[0] = str2[0] = '\0';
    adrs = 0;
    while ((ch = agetc(af)) != EOF)
    {
	sprintf (&str1[strlen(str1)],"%2X ", ch);
	sprintf (&str2[strlen(str2)],"%c%c", achar[ch*2], achar[ch*2+1]);
	adrs++;
	if (!(adrs % 8))
	{
	    fprintf (fp,"%6X: %s  %s\n", adrs-8, str1, str2);
	    str1[0] = str2[0] = '\0';
	}
    }
    fprintf (fp,"%6X: %-24s  %s\n", adrs-8, str1, str2);
}


/* traverse the tsl table and set/clear the indicated sectors */
int traverse_tsl(struct afile* af, int operation)
{
    int	t, s;
    int sectors = 0;

    /* traverse the tsl, if valid */
    while(af->ts_t != 0 || af->ts_s != 0)
    {
	/* check for full tsl */
	if (++af->ts_ent >= byteat(af->d, 0x11, 0, 0x27))
	{
	    if (operation == DELETE)
	    {
		/* free up the previous tsl */
		free_sector(af->d, af->ts_t, af->ts_s);
	    }

	    /* access next tsl */
	    af->ts_ent = 0;
	    t = byteat(af->d, af->ts_t, af->ts_s, 1);
	    s = byteat(af->d, af->ts_t, af->ts_s, 2);
	    if (t == 0 && s == 0)
		break;

	    if (operation == DELETE)
	    {
		af->ts_t = t;
		af->ts_s = s;
	    }
	    else
	    {
		if (isfree(af->d, t, s))
		{
		    alloc_sector(af->d, t, s);
		    af->ts_t = t;
		    af->ts_s = s;
		    sectors++;
		}
		else
		{
		    byteat(af->d, af->ts_t, af->ts_s, 1) = 0;
		    byteat(af->d, af->ts_t, af->ts_s, 2) = 0;
		    break;
		}
	    }
	}

	/* get next track/sector entry */
	af->dt_t = byteat(af->d, af->ts_t, af->ts_s, 12 + af->ts_ent * 2);
	af->dt_s = byteat(af->d, af->ts_t, af->ts_s, 12 + af->ts_ent * 2 + 1);
 
	if (operation == DELETE)
	{
	    if (af->dt_t == 0 && af->dt_s == 0)	/* end of tsl */
	    {
		/* free up the tsl */
		free_sector(af->d, af->ts_t, af->ts_s);
		break;
	    }
	    else	/* free up track/sector entry */
	    {
		/* free up sectors in the tsl table */
		free_sector(af->d, af->dt_t, af->dt_s);
	    }
	}
	else	/* UNDELETE */
	{
	    /* validate track/sector entry */
	    if (af->dt_t == 0 && af->dt_s == 0)
		break;

	    if (isfree(af->d, af->dt_t, af->dt_s))
	    {
		alloc_sector(af->d, af->dt_t, af->dt_s);
		sectors++;
	    }
	    else
	    {
		byteat(af->d, af->ts_t, af->ts_s, 12 + af->ts_ent * 2) = 0;
		byteat(af->d, af->ts_t, af->ts_s, 12 + af->ts_ent * 2 + 1) = 0;
		break;
	    }
	}
    }
    return (sectors);
}

int dtraverse_tsl(struct afile* af, int operation)
{
    int	t, s;
    int sectors = 0;

    /* traverse the tsl, if valid */
    while(af->ts_t != 0 || af->ts_s != 0)
    {
	/* check for full tsl */
	if (++af->ts_ent >= byteat(af->d, 0x11, 0, 0x27))
	{
	    /* free up the previous tsl */
	    free_sector(af->d, af->ts_t, af->ts_s);

	    /* access next tsl */
	    af->ts_ent = 0;
	    t = byteat(af->d, af->ts_t, af->ts_s, 1);
	    s = byteat(af->d, af->ts_t, af->ts_s, 2);
	    if (t == 0 && s == 0)
		break;
	    af->ts_t = t;
	    af->ts_s = s;
	}

	/* get next track/sector entry */
	af->dt_t = byteat(af->d, af->ts_t, af->ts_s, 12 + af->ts_ent * 2);
	af->dt_s = byteat(af->d, af->ts_t, af->ts_s, 12 + af->ts_ent * 2 + 1);
 
	if (af->dt_t == 0 && af->dt_s == 0)	/* end of tsl */
	{
	    /* free up the tsl */
	    free_sector(af->d, af->ts_t, af->ts_s);
	    break;
	}
	else	/* free up track/sector entry */
	{
	    /* free up sectors in the tsl table */
	    free_sector(af->d, af->dt_t, af->dt_s);
	}
    }
    return (sectors);
}
int utraverse_tsl(struct afile* af, int operation)
{
    int	t, s;
    int sectors = 0;

    if (isfree(af->d, af->ts_t, af->ts_s))
    {
	alloc_sector(af->d, af->ts_t, af->ts_s);
	sectors++;
    }
    else
    {
	byteat(af->d, af->dir_t, af->dir_s, af->dir_off) = 0;
	byteat(af->d, af->dir_t, af->dir_s, af->dir_off+1) = 0;
    }

    /* traverse the tsl, if valid */
    while(af->ts_t != 0 || af->ts_s != 0)
    {
	/* check for full tsl */
	if (++af->ts_ent >= byteat(af->d, 0x11, 0, 0x27))
	{
	    /* access next tsl */
	    af->ts_ent = 0;
	    t = byteat(af->d, af->ts_t, af->ts_s, 1);
	    s = byteat(af->d, af->ts_t, af->ts_s, 2);
	    if (t == 0 && s == 0)
		break;

	    if (isfree(af->d, t, s))
	    {
		alloc_sector(af->d, t, s);
		af->ts_t = t;
		af->ts_s = s;
		sectors++;
	    }
	    else
	    {
		byteat(af->d, af->ts_t, af->ts_s, 1) = 0;
		byteat(af->d, af->ts_t, af->ts_s, 2) = 0;
		break;
	    }
	}

	/* get next track/sector entry */
	af->dt_t = byteat(af->d, af->ts_t, af->ts_s, 12 + af->ts_ent * 2);
	af->dt_s = byteat(af->d, af->ts_t, af->ts_s, 12 + af->ts_ent * 2 + 1);
 
	/* validate track/sector entry */
	if (af->dt_t == 0 && af->dt_s == 0)
	    break;

	if (isfree(af->d, af->dt_t, af->dt_s))
	{
	    alloc_sector(af->d, af->dt_t, af->dt_s);
	    sectors++;
	}
	else
	{
	    byteat(af->d, af->ts_t, af->ts_s, 12 + af->ts_ent * 2) = 0;
	    byteat(af->d, af->ts_t, af->ts_s, 12 + af->ts_ent * 2 + 1) = 0;
	    break;
	}
    }
    return (sectors);
}
