// -------------------------------------------------------------------------------------------
// DISKLIST - print directory listing of DMS2 disk image
// -------------------------------------------------------------------------------------------

/*
 * (C) Copyright 2004, Brian Knittel.
 * You may freely use this program, but: it offered strictly on an AS-IS, AT YOUR OWN
 * RISK basis, there is no warranty of fitness for any purpose, and the rest of the
 * usual yada-yada. Please keep this notice and the copyright in any distributions
 * or modifications.
 */

// -------------------------------------------------------------------------------------------
// HISTORY
// -------------------------------------------------------------------------------------------
// 24-Nov-2004 Written. It would be nice to make this distinguish a DMS disk from
//                      other potential disk formats (e.g. APL\1130).
//
// 03-Dec-2004 Split get_let and print_let so we could get listings for specific files.
// 06-Dec-2004 Added printout of detailed file info (print_xxx_info) and dump, and listing of calls
// 21-Dec-2006 Added file type column in standard output mode
// -------------------------------------------------------------------------------------------

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "util_io.h"

// -------------------------------------------------------------------------------------------
// DEFINITIONS
// -------------------------------------------------------------------------------------------

typedef int            BOOL;                        // boolean
typedef unsigned short uint16;                      // unsigned 16-bit integer
typedef short          int16;                       // signed   16-bit integer

#define TRUE  1                                     // BOOL values
#define FALSE 0

#define ALLOCATE(obj) ((obj *) calloc(1, sizeof(obj)))      // macro to allocate an object and return pointer to it

#define SEC_WORDS       320                         // useful words per sector
#define SEC_BYTES       640                         // bytes per sector
#define PHY_WORDS       321                         // physical words per sector
#define SLET_LENGTH     160                         // size of SLET (2 sectors of 4 words per entry)
#define SEC_BLOCKS       16                         // disk blocks per sector
#define BLK_WORDS        20                         // size of a "disk block", a sub-sector
#define BLK_BYTES        40

#define THOUSANDS_SEP ','                           // thousands separator (eg. 9,999,999)
                                                    // (in Europe, define as '.', or comment out definition)

typedef struct tag_letentry {                       // linked list node for directory entry
    struct tag_letentry *next;
    char name[6];                                   // file name (1-5 chars)
    uint16 filetype;                                // file image type
    uint16 dbcount;                                 // length in DMS "disk blocks" (20 words per block)
    uint16 dbaddr;                                  // disk block address
    struct tag_letentry *master;                    // master entry, if this is an alternate name entry
    BOOL dummy;                                     // TRUE if this is a 1DUMY entry
} LETENTRY;

typedef struct tag_filearg {                        // node in linked list of filename arguments
    struct tag_filearg *next;
    char *name;
} FILEARG;

static char *progtype_nm[16] = {                    // DMS2 program types for DSF format files
    "Undefined",        "Mainline, absolute",   "Mainline, relocatable",    "LIBF Subprogram",
    "CALL Subprogram",  "LIBF Interrupt Service Subroutine (ISS)",          
                        "CALL Interrupt Service Subroutine (ISS)",  "Interrupt Level Subroutine (ILS)",
    "Undefined",        "Undefined",            "Undefined",                "Undefined",
    "Undefined",        "Undefined",            "Undefined",                "Undefined"
};

struct {                                            // DMS2 program subtypes for DSF format files
    unsigned progtype;
    unsigned subtype;
    char *descr;
} subtype_nm[] = {
    3, 0, "In-core subprogram",
    3, 1, "FORTRAN Disk IO subroutine",
    3, 2, "Arithmetic subroutine",
    3, 3, "FORTRAN non-disk IO and \"Z\" conversion subroutine",
    5, 3, "\"Z\" device suboutine",
    5, 0, NULL,                                     // NULL suppresses printing of subtype
    4, 0, "In-core subprogram",
    4, 8, "Function subprogram",
    7, 1, "Dummy ILS02 or ILS04",
};
#define N_SUBTYPE_NMS (sizeof(subtype_nm) / sizeof(subtype_nm[0]))

#pragma pack(1)

typedef struct tag_dsf_program_header {             // header block of a DSF format file, disk layout
    uint16      zero1;
    uint16      checksum;
    uint16      type;
    uint16      proglen;                // effective length, terminal address
    uint16      commonlen;              // length of common
    uint16      hdr_len9;               // length of this header - 9
    uint16      zero2;
    uint16      dblen;                  // length of program including header in disk blocks (20 wds)
    uint16      fortran_info;
    union {
        struct {                        // normal programs: entry point information. 1-15. Actual number is hdr_len9/3
            uint16  name[2];
            uint16  addr;
        } entry[15];
        struct {                        // ISS (types 5 and 6)
            uint16  name[2];
            uint16  addr;
            uint16  iss_50;             // ISS number + 50
            uint16  issnumber;          // ISS number
            uint16  nlevels;            // # of levels required (1 or 2)
            uint16  level[2];           // interrupt level associated with interrupt (nlevels entries used)
        } iss;
        struct {                        // ILS (type 7)
            uint16  name[2];
            uint16  addr;
            uint16  level;              // interrupt level
        } ils;
    } x;
} DSF_PROGRAM_HEADER;

typedef struct tag_dci_program_header {         // header of a DCI (core image) format file, disk layout
    uint16  xeqa;                       // execute address
    uint16  cmon;                       // length of COMMON
    uint16  dreq;                       // disk IO indicator, /FFFF for DISKZ, 0000 for DISK1, 0001 for DISKN
    uint16  file;                       // number of files defined
    uint16  hwct;                       // length of core image header in words
    uint16  lsct;                       // sector count of files in system WS
    uint16  ldad;                       // loading address of core load
    uint16  xctl;                       // exit control address for DISK1/N
    uint16  tvwc;                       // length of transfer vector in words
    uint16  wcnt;                       // length, in words of the core load, core image header and transfer vector
    uint16  xr3x;                       // setting for index register 3 during execution of core load
    uint16  itv[6];                     // ITV (values of words 8-13 during execution)
    uint16  reserved1;
    uint16  ibt[8];                     // IBT for ILS04. interrupt entry for ISS of 1231 (3 words), 1403, 2501, 1442, keyboard/prt, 1134/1055 respectively
    uint16  ovsw;                       // sector count of LOCALs/SOCALs
    uint16  core;                       // core size of system on which core load was built
    uint16  reserved2[2];
    uint16  hend;                       // last word of header
} DCI_PROGRAM_HEADER;

#pragma pack()

// -------------------------------------------------------------------------------------------
// GLOBALS
// -------------------------------------------------------------------------------------------

#define FILETYPE_DSF        0                       // DMS2 filetypes. Type 1 is undefined
#define FILETYPE_1          1
#define FILETYPE_DCI        2
#define FILETYPE_DDF        3

uint16 defective[3] = {0xFFFF, 0xFFFF, 0xFFFF};     // defective cylinder table, number is 1st sector number of bad cylinder
FILE *fd;                                           // stream for open disk image file
BOOL verbose = FALSE;                               // verbose switch
BOOL show_all = FALSE;                              // switch to display alternate file entries
BOOL dumpslet = FALSE;                              // dump SLET switch
BOOL do_dump = FALSE;
char *ftname[4] = {"DSF", "???", "DCI", "DDF"};     // DMS2 filetype names
    
LETENTRY *flet = NULL, *let = NULL;                 // pointers to contents of FLET and LET

#pragma pack(1)                                     // (don't pad struct elements)

struct {                                            // buffer for one sector
    uint16 secno;
    uint16 data[SEC_WORDS];
} sector;

struct {                                            // structure of the SLET on disk
    uint16  id;
    uint16  addr;
    uint16  size;
    uint16  secno;
} slet[SLET_LENGTH];

struct {                                            // DCOM sector
    uint16  _dummy0;
    uint16  _dummy1;
    uint16  _dummy2;
    uint16  _dummy3;
    uint16  name[2];                                //   4  name of program
    uint16  dbct;                                   //   6  disk block count of program
    uint16  fcnt;                                   //   7  files indicator
    uint16  sysc;                                   //   8  system cartridge indicator switch
    uint16  jbsw;                                   //   9  temporary job switch (nonzero = JOB T)
    uint16  cbsw;                                   //  10  clb switch (nonzero = storeci)
    uint16  lcnt;                                   //  11  local indicator (# of locals)
    uint16  mpsw;                                   //  12  core map desired switch
    uint16  mdf1;                                   //  13  no. of dup ctrl rcds (modif)
    uint16  mdf2;                                   //  14  addr of modif buffer
    uint16  ncnt;                                   //  15  nocal indicator
    uint16  enty;                                   //  16  rel entry addr of program
    uint16  rp67;                                   //  17  1442-5 switch (nonzero = mod 6 or 7)
    uint16  todr;                                   //  18  -to- wk stg drive code
    uint16  frdr;                                   //  19  -from- wk stg drive code
    uint16  fhol;                                   //  20  addr of largest hole in fxa
    uint16  fsze;                                   //  21  blk cnt largest hole in fxa
    uint16  uhol;                                   //  22  addr of largest hole in ua
    uint16  usze;                                   //  23  blk cnt largest hole in ua
    uint16  dcsw;                                   //  24  dup call switch
    uint16  piod;                                   //  25  principal IO device indicator
    uint16  pptr;                                   //  26  print print device indicator
    uint16  ciad;                                   //  27  sctr 0 loc of cil sctr addr
    uint16  cain;                                   //  28  avail cartridge indicator
    uint16  grph;                                   //  29  2250 indicator
    uint16  gcnt;                                   //  30  g2260 count
    uint16  locw;                                   //  31  local call locals sw
    uint16  x3sw;                                   //  32  special ils sw
    uint16  ecnt;                                   //  33  equat count
    uint16  _dummy34;                               //  34
    uint16  andu[5];                                //  35  end of UA address (adj)
    uint16  bndu[5];                                //  40  end of UA address (base)
    uint16  fpad[5];                                //  45  file protected address
    uint16  pcid[5];                                //  50  available cartridge IDs (physical drvs 0..4)
    uint16  cidn[5];                                //  55  cartridge ID            (logical drvs 0..4)
    uint16  ciba[5];                                //  60  sector address of CIB
    uint16  scra[5];                                //  65  sector address of SCRA
    uint16  fmat[5];                                //  70  format of program in WS
    uint16  flet[5];                                //  75  FLET sector address
    uint16  ulet[5];                                //  80  LET sector address
    uint16  wsct[5];                                //  85  BLK count of program in WS
    uint16  cshn[5];                                //  90  1+sctr addr of end of cusn.
} dcom;

#pragma pack()

BOOL is_system = FALSE;                                 // TRUE if this is a system disk

// NOTE: in DMS, disk blocks are 1/16 of a sector (20 words) -- DMS suballocates sectors for files. Some
// files have to start on a sector boundary, and there are 1DUMY entries for the little lost bits. The last
// 1DUMY entry in the LET is the size of Working Storage.

// -------------------------------------------------------------------------------------------
// PROTOTYPES
// -------------------------------------------------------------------------------------------

void getsec (uint16 secno);                             // read sector by number
void getdata (void *buf, uint16 dbaddr, uint16 offset, uint16 nwords);  // read data from file relative to its disk block address
void bail (char *msg);                                  // print error message and exit
void print_slet (void);                                 // print contents of SLET
void get_let (LETENTRY **listhead, uint16 secno);       // read FLET or LET, building linked list
void print_let (char *title, LETENTRY *listhead);       // print contents of FLET or LET
void list_named_files (char *name, char *image) ;       // print info for specified file(s)
void print_onefile (LETENTRY *entry, BOOL in_flet);     // print detailed info about one particular file
int  ebcdic_to_ascii (int ch);                          // convert EBCDIC character to ASCII
void convert_namecode (uint16 *namecode, char *name);   // convert DMS name code words into ASCII filename
char *upcase (char *str);                               // convert string to upper case
void commas (int n, int width);                         // print number n with commas
char *astring (char *str);                              // allocate memory for and return copy of string
void print_dsf_info (LETENTRY *entry);                  // print information about a Disk System Format file
void print_dci_info (LETENTRY *entry);                  // print information about a Disk Core Image file
void print_ddf_info (LETENTRY *entry);                  // print information about a Disk Data Format file
char * file_progtype (LETENTRY *entry);                 // description of module type

// -------------------------------------------------------------------------------------------
// main - main routine
// -------------------------------------------------------------------------------------------

int main (int argc, char **argv)
{
    int i;
    char *arg, cartid[10];
    char *image = NULL;
    FILEARG *fileargs = NULL, *filearg, *filearg_tail = NULL;
    char *usestr = "Usage: disklist [-sadv] diskfile [filename ...]\n"
                   "\n"
                   "Lists contents of fixed and user area directories in IBM 1130 DMS 2\n"
                   "disk image file \"diskfile\". With the optional filename argument(s)\n"
                   "(1-5 letters), prints detailed information about the named file(s).\n"
                   "Wildcard characters ? and * may be specfied in the filename."
                   "\n"
                   "  -s  dump SLET in addition to fixed and user areas\n"
                   "  -a  dump additional information including alternate entries and addresses\n"
                   "      For named file(s), prints information about entry points and calls\n"
                   "  -d  dumps contents of named file(s) in hex\n"
                   "  -v  verbose mode, prints internal information\n";

    for (i = 1; i < argc; i++) {                            // scan command line arguments
        arg = argv[i];
        if (*arg == '-') {                                  // command line switch
            arg++;                                          // skip over the -
            while (*arg) {                                  // process all flags
                switch (*arg++) {
                    case 'v':
                        verbose = TRUE;                     // -v turns on verbose mode
                        break;
                    case 'a':
                        show_all = TRUE;                    // -a turns on listing of alternate file entries & pad spaces
                        break;
                    case 's':                               // -s turns on dump slet
                        dumpslet = TRUE;
                        break;
                    case 'd':
                        do_dump = TRUE;
                        break;
                    default:
                        bail(usestr);                       // unrecognized flag
                }
            }
        }
        else if (image == NULL)
            image = arg;                                    // first name is the name of disk image file
        else {
            filearg = ALLOCATE(FILEARG);                    // subsequent names are filename arguments,
            filearg->name = upcase(astring(arg));           // copy to a FILEARG object (as uppercase)
            filearg->next = NULL;

            if (fileargs == NULL)                           // add to end of linked list
                fileargs = filearg;
            else
                filearg_tail->next = filearg;

            filearg_tail = filearg;
        }
    }

    if (image == NULL)                                      // filename was not specified
        bail(usestr);

    if ((fd = fopen(image, "rb")) == NULL) {                // open file in binary mode
        perror(image);                                      // print reason for open failure and exit
        return 1;
    }

    getsec(0);                                              // get sector 0, which has defective cylinder table

    defective[0] = sector.data[0];                          // load defective cylinder table
    defective[1] = sector.data[1];
    defective[2] = sector.data[2];

    if (verbose)
        printf("Defective cylinder table: %04x %04x %04x\n", defective[0], defective[1], defective[2]);

    printf("Filename: %s", image);

    sprintf(cartid, "%04x", sector.data[3]);                // display cartridge ID in upper case
    printf("   Cartridge ID: %s", upcase(cartid));
    if (show_all)
        printf("   Copy: number %d", sector.data[4]);

    printf("\n\n");

    getsec(1);                                              // get sector 1, save to DCOM
    memcpy(&dcom, sector.data, min(sizeof(dcom), SEC_BYTES));

    is_system = dcom.sysc != 0;                             // is this a system cartridge?

    if (dumpslet) {                                         // display SLET
        if (is_system) {
            getsec(3);
            memcpy(slet+0,  sector.data, SEC_BYTES);
            getsec(4);
            memcpy(slet+80, sector.data, SEC_BYTES);

            print_slet();
        }
        else
            printf("(Not a system cartridge, no SLET)\n\n");
    }

    if (dcom.flet[0] != 0)                                  // do we have a FLET?
        get_let(&flet, dcom.flet[0]);                       // read it

    get_let(&let, dcom.ulet[0]);                            // read LET

    if (fileargs == NULL) {                                 // if there are no filename arguments
        if (flet != NULL)                                   // print FLET and LET
            print_let("FIXED AREA", flet);

        print_let("USER AREA", let);
    }
    else {                                                  // print information for specified file(s)
        for (filearg = fileargs; filearg != NULL; filearg = filearg->next)
            list_named_files(filearg->name, image);
    }

    return 0;
}

// -------------------------------------------------------------------------------------------
// upcase - force a string to uppercase (ASCII)
// -------------------------------------------------------------------------------------------

char *upcase (char *str)
{
    char *s;

    for (s = str; *s; s++) {
        if (*s >= 'a' && *s <= 'z')
            *s -= 32;
    } 

    return str;
}

// -------------------------------------------------------------------------------------------
// commas - print n as a decimal number with commas; width is minimum width
// -------------------------------------------------------------------------------------------

void commas (int n, int width)
{
    char fmt[20];
#ifdef THOUSANDS_SEP
    int nchar;
    char tmp[20], *cin, *cout;

    sprintf(tmp, "%d", n);                  // format number n into string
    nchar = strlen(tmp);                    // get length of string

    for (cin = tmp, cout = fmt; *cin; ) {   // scan through the formatted number
        *cout++ = *cin++;                   // output digit
        --nchar;                            // get number of digits left
        if (nchar > 0 && (nchar % 3) == 0)
            *cout++ = THOUSANDS_SEP;        // if there is a multiple of three digits left, emit a comma
    }
    *cout = '\0';                           // terminate string

#else                                       // THOUSANDS_SEP is undefined, output number w/o commas

    sprintf(fmt, "%d", n);

#endif

    width -= strlen(fmt);                   // get width shortage
    while (--width >= 0)                    // output spaces if necessary
        putchar(' ');

    fputs(fmt, stdout);                     // print formatted number
}

// -------------------------------------------------------------------------------------------
// bail - print fatal error message and exit
// -------------------------------------------------------------------------------------------

void bail (char *msg)
{
    fprintf(stderr, "%s\n", msg);
    exit(1);
}

// -------------------------------------------------------------------------------------------
// getsec - read desired sector
// -------------------------------------------------------------------------------------------

void getsec (uint16 secno)
{
    int i, phys_sec;
    static uint16 cur_sec = 0xFFFF;

    if (secno == cur_sec)                                   // see if we already have the sector. Presumes
        return;                                             // we haven't modified its contents!

    cur_sec = secno;                                        // remember current sector

    phys_sec = secno;                                       // physical sector

    for (i = 0; i < 3; i++) {                               // bump cylinder if it's past any in the defective cylinder list
        if (secno >= defective[i])                          // (use logical secno for comparisons, not physical secno)
            phys_sec += 8;
        else
            break;
    }

    fseek(fd, phys_sec*PHY_WORDS*2, SEEK_SET);              // jump to translated sector

    if (fxread(&sector, 2, PHY_WORDS, fd) != PHY_WORDS)     // fxread handles flipping data on little-endian machines
        bail("error reading disk image");

    if (sector.secno != secno) {                            // verify that 1st word in sector is sector number
        fprintf(stderr, "* expected sector number /%04x, got /%04x\n", secno, sector.secno);
        bail("disk image is corrupt");
    }
}

// -------------------------------------------------------------------------------------------
// getdata -- read data from file relative to its disk block address
// -------------------------------------------------------------------------------------------

void getdata (void *buf, uint16 dbaddr, uint16 offset, uint16 nwords)
{
    uint16 secno, nsec, nw;

    if (nwords == 0)
        return;

    secno   = dbaddr / SEC_BLOCKS;                      // desired sector number from dbaddr
    dbaddr -= SEC_BLOCKS*secno;                         // # of blocks offset within that sector
    offset += dbaddr*BLK_WORDS;                         // add offset in words

    nsec    = offset / SEC_WORDS;                       // turn offset into integer sectors
    secno  += nsec;                                     // bump sector number
    offset -= nsec*SEC_WORDS;                           // ultimate offset within sector

    for (;;) {
        getsec(secno);                                  // read desired sector
        nw = min(SEC_WORDS-offset, nwords);             // number of words to copy from this sector
        memcpy(buf, &sector.data[offset], nw*2);        // copy the data

        if ((nwords -= nw) <= 0)                        // decrement remaining word count
            break;

        secno++;                                        // bump sector
        offset = 0;                                     // no offset in subsequent sector(s)
        ((uint16 *) buf) += nw;                         // bump buffer pointer
    }
}

// -------------------------------------------------------------------------------------------
// print_slet - list the contents of the SLET
// -------------------------------------------------------------------------------------------

struct {
    unsigned int id;
    char *name;
} slet_phase[] = {
    0x01, "@DDUP DUPCO *** DUP",                            // DMS R1V12 phase ID's and names
    0x02, "@DCTL DUP CONTROL - PART 1",
    0x03, "@STOR STORE",
    0x04, "@FILQ FILE EQUATE",
    0x05, "@DUMP DUMP",
    0x06, "@DL/F DUMP LET/FLET",
    0x07, "@DLTE DELETE",
    0x08, "@DFNE DEFINE",
    0x09, "@EXIT DEXIT",
    0x0A, "@CFCE CARD INTERFACE",
    0x0B, "@DU11 KEYBOARD INTERFACE",
    0x0C, "@DU12 PAPER TAPE INTERFACE",
    0x0D, "@DU13 DUP UPCOR",
    0x0E, "@DU14 DUP PRINCIPAL I/O",
    0x0F, "@DU15 DUP PRINCIPAL I/O SANS KB",
    0x10, "@DU16 DUP PAPER TAPE I/O",
    0x11, "@PRCI PRE CORE IMAGE",
    0x12, "@DU18 DUP RESERVED",
    0x1F, "@FR01 INPUT *** FORTRAN COMPILER",
    0x20, "@FR02 CLASSIFIER",
    0x21, "@FR03 CHECK ORDER/STMNT NUMBER",
    0x22, "@FR04 COMMON/SUBROUTINE OR FUNC",
    0x23, "@FR05 DIM/REAL, INTEGER, EXTERNAL",
    0x24, "@FR06 REAL CONSTANTS",
    0x25, "@FR07 DEFN FILE, CALL LINK/EXIT",
    0x26, "@FR08 VARIABLES AND STMNT FUNC",
    0x27, "@FR09 DATA STATEMENT",
    0x28, "@FR10 FORMAT",
    0x29, "@FR11 SUBSCRIPT DECOMPOSITION",
    0x2A, "@FR12 ASCAN I",
    0x2B, "@FR13 ASCAN II",
    0x2C, "@FR14 DO, CONTINUE, ETC",
    0x2D, "@FR15 SUBSCRIPT OPTIMIZE",
    0x2E, "@FR16 SCAN",
    0x2F, "@FR17 EXPANDER I",
    0x30, "@FR18 EXPANDER II",
    0x31, "@FR19 DATA ALLOCATION",
    0x32, "@FR20 COMPILATION ERRORS",
    0x33, "@FR21 STATEMENT ALLOCATION",
    0x34, "@FR22 LIST STATEMENT ALLOCATION",
    0x35, "@FR23 LIST SYMBOLS",
    0x36, "@FR24 LIST CONSTANTS",
    0x37, "@FR25 OUTPUT I",
    0x38, "@FR26 OUTPUT II",
    0x39, "@FR27 RECOVERY",
    0x3A, "DUMMY DUMMY NAME",
    0x3B, "DUMMY DUMMY NAME",
    0x3C, "DUMMY DUMMY NAME",
    0x51, "@QCTL PROCESS CTL CDS *** COBOL COMPILER ",
    0x52, "@QTXT SOURCE TEXT REDUCTION",
    0x53, "@QLIT LITERAL ALLOCATION",
    0x54, "@QDTA DATA DIVISION PROCESSING",
    0x55, "@QPRO PROCEDURE DIV SCAN",
    0x56, "@QGEN GENERATE INST STRINGS",
    0x57, "@QOBJ PRODUCE DSF-MODULE",
    0x58, "@QERR MAP/DIAGNOSTIC OUTPUT",
    0x59, "@QEND COMPILE TERMINATION",
    0x5A, "@QSER PRODUCE SERVICEABILITY",
    0x5B, "@QXR1",
    0x5C, "@QXR2",
    0x6E, "@SUP1 MONITOR CTRL RCD ANALYZER *** SUPERVISOR",
    0x6F, "@SUP2 JOB RECORD PROCESSING",
    0x70, "@SUP3 DELETE TEMPOTARY LET",
    0x71, "@SUP4 XEQ RECORD PROCESSING",
    0x72, "@SUP5 SCR PROCESSING",
    0x73, "@SUP6 SYSTEM DUMP PROGRAM",
    0x74, "@SUP7 AUXILIARY SUPERVISOR",
    0x78, "@CLB1 PHASE 1 *** CORE LOAD BUILDER",
    0x79, "@CLB2 PHASE 2",
    0x7A, "@CLB3 PHASE 3",
    0x7B, "@CLB4 PHASE 4",
    0x7C, "@CLB5 PHASE 5",
    0x7D, "@CLB6 PHASE 6",
    0x7E, "@CLB7 PHASE 7",
    0x7F, "@CLB8 PHASE 8",
    0x80, "@CLB9 PHASE 9",
    0x81, "@CLBA PHASE 10",
    0x82, "@CLBB PHASE 11",
    0x83, "@CLBC PHASE 12",
    0x84, "@CLBD PHASE 13 (GRAPHICS)",
    0x8C, "@1403 1403 SUBR *** SYSTEM DEVICE DRIVERS",
    0x8D, "@1132 1132 SUBR",
    0x8E, "@CPTR CONSOLE PRINTER SUBR",
    0x8F, "@2501 2501 SUBR",
    0x90, "@1442 1442 SUBR",
    0x91, "@1134 1134 SUBR",
    0x92, "@KBCP KB/CONSOLE PRINTER SUBR",
    0x93, "@CDCV 2501/1442 CONVERSION SUBR",
    0x94, "@PTCV 1134 CONVERSION SUBR",
    0x95, "@KBCV KB/CP CONVERSION SUBR",
    0x96, "@DZID DISKZ",
    0x97, "@D1ID DISK1",
    0x98, "@DNID DISKN",
    0x99, "@PPRT PRINCIPAL PRINT SUBROUTINE",
    0x9A, "@PIWK PRINCIPAL INPUT SUBROUTINE",
    0x9B, "@PIXK PRINCIPAL INPUT W/O KB",
    0x9C, "@PCWK PRINCIPAL CONV W/ KEYBOARD",
    0x9D, "@PCXK PRINCIPAL CONV W/O KEYBOARD",
    0xA0, "@CIL1 PHASE 1 *** CORE IMAGE LOADER",
    0xA1, "@CIL2 PHASE 2",
    0xB0, "@RG00 PHASE 0 *** RPG COMPILER",
    0xB1, "@RG02 PHASE 2",
    0xB2, "@RG04 PHASE 4",
    0xB3, "@RG06 PHASE 6",
    0xB4, "@RG08 PHASE 8",
    0xB5, "@RG10 PHASE 10",
    0xB6, "@RG12 PHASE 12",
    0xB7, "@RG14 PHASE 14",
    0xB8, "@RG16 PHASE 16",
    0xB9, "@RG17 PHASE 17",
    0xBA, "@RG19 PHASE 19",
    0xBB, "@RG20 PHASE 20",
    0xBC, "@RG21 PHASE 21",
    0xBD, "@RG22 PHASE 22",
    0xBE, "@RG24 PHASE 24",
    0xBF, "@RG26 PHASE 26",
    0xC0, "@RG28 PHASE 28",
    0xC1, "@RG32 PHASE 32",
    0xC2, "@RG34 PHASE 34",
    0xC3, "@RG36 PHASE 36",
    0xC4, "@RG38 PHASE 38",
    0xC5, "@RG40 PHASE 40",
    0xC6, "@RG42 PHASE 42",
    0xC7, "@RG44 PHASE 44",
    0xC8, "@RG46 PHASE 46",
    0xC9, "@RG52 PHASE 52",
    0xCA, "@RG54 PHASE 54",
    0xCB, "@RG58 PHASE 58",
    0xCC, "@RG60 PHASE 60",
    0xCD, "@DCL2 *** DUP CONTROL - PART 2",
    0xCE, "@DMUP MACRO UPDATE PROGRAM",
    0xCF, "@AS00 PHASE 0 *** ASSEMBLER",
    0xD0, "@ACNV CARD CONVERSION",
    0xD1, "@AS10 PHASE 10",
    0xD2, "@AS11 PHASE 11",
    0xD3, "@AS12 PHASE 12",
    0xD4, "@AERM ERROR MESSAGES",
    0xD5, "@AS01 PHASE 1",
    0xD6, "@AS1A PHASE 1A",
    0xD7, "@ASYM SYSTEM SYMBOL TABLE",
    0xD8, "@AS03 PHASE 3",
    0xD9, "@AS04 PHASE 4",
    0xDA, "@AS02 PHASE 2",
    0xDB, "@AS2A PHASE 2A",
    0xDC, "@AS09 PHASE 9",
    0xDD, "@AS05 PHASE 5",
    0xDE, "@AS06 PHASE 6",
    0xDF, "@AS07 PHASE 7",
    0xE0, "@AS7A PHASE 7A",
    0xE1, "@AS08 PHASE 8",
    0xE2, "@AS8A PHASE 8A",
    0xE3, "@APCV CARD PUNCH CONVERSION",
    0xE4, "@AINT INTERMEDIATE DISK OUTPT",
    0xE5, "@ASAA PHASE 10A",
    0xE6, "@ASGR PHASE 13 GRAPHICS",
    0xE7, "@ADIV DIVISION OPERATOR",
    0xE8, "@AMCC MACRO CONTROL CARDS III",
    0xE9, "@AM01 MACRO PHASE 1",
    0xEA, "@AM1A MACRO PHASE 1A",
    0xEB, "@AM1B MACRO PHASE 1B",
    0xEC, "@AM02 MACRO PHASE 2",
    0xED, "@AM2A MACRO PHASE 2A",
    0xEE, "@AM2B MACRO PHASE 2B",
    0xEF, "@AM03 MACRO PHASE 3",
    0xF0, "@AM3A MACRO PHASE 3A",
    0xF1, "@AM3B MACRO PHASE 3B",
    0xF2, "@AX01 CROSS REF - PART 1",
    0xF3, "@AX2A CROSS REF - PART 2A",
    0xF4, "@AX2B CROSS REF - PART 2B",
    0xF5, "@AX2C CROSS REF - PART 2C",
    0xF6, "@AX03 CROSS REF - PART 3",
    0x100, "@AS00 *** MSP7 ASSEMBLER",
    0x101, "@ACNV",
    0x102, "@AS10",
    0x103, "@AS11",
    0x104, "@AS12",
    0x105, "@AERM",
    0x106, "@AS01",
    0x107, "@AS1A",
    0x108, "@ASYM",
    0x109, "@AS03",
    0x10A, "@AS04",
    0x10B, "@AS02",
    0x10C, "@AS2A",
    0x10D, "@AS09",
    0x10E, "@AS05",
    0x10F, "@AS06",
    0x110, "@AS07",
    0x111, "@AS7A",
    0x112, "@AS08",
    0x113, "@AS8A",
    0x114, "@APCV",
    0x115, "@AINT",
    0x116, "@ASAA",
    0x117, "@ASGR",
    0x118, "@ADIV",
    0x119, "@AMCC",
    0x11A, "@AM01",
    0x11B, "@AM1A",
    0x11C, "@AM1B",
    0x11D, "@AM02",
    0x11E, "@AM2A",
    0x11F, "@AM2B",
    0x120, "@AM03",
    0x121, "@AM3A",
    0x122, "@AM3B",
    0x123, "@AX01",
    0x124, "@AX2A",
    0x125, "@AX2B",
    0x126, "@AX2C",
    0x127, "@AX03",
    0x128, "@ASP7",
    0xFFFF, ""
};
#define N_SLET_PHASES (sizeof(slet_phase)/sizeof(slet_phase[0]) - 1)

void print_slet (void)
{
    int i, j = 0;

    printf("SLET (System Logical Equivalence Table)\n\n");  // dump SLET
    printf("ID   Addr Size Sect Description\n");
    printf("---- ---- ---- ---- -----------------------\n");

    for (i = 0; i < SLET_LENGTH; i++) {
        if (slet[i].id == 0 && slet[i].secno == 0)
            break;

        printf("%04x %04x %04x %04x ", slet[i].id, slet[i].addr, slet[i].size, slet[i].secno);

        while (j < N_SLET_PHASES && slet_phase[j].id < slet[i].id)
            j++;                                            // skip to entry in slet_phase name table

        printf("%s\n", (slet_phase[j].id == slet[i].id) ? slet_phase[j].name : "?");
    }

    putchar('\n');
}

// -------------------------------------------------------------------------------------------
// ebcdic_to_ascii - convert EBCDIC character to ASCII (ignores controls)
// -------------------------------------------------------------------------------------------

int ebcdic_to_ascii (int ch)
{
    int j;
    static int ascii_to_ebcdic_table[128] = {
    //
        0x00,0x01,0x02,0x03,0x37,0x2d,0x2e,0x2f, 0x16,0x05,0x25,0x0b,0x0c,0x0d,0x0e,0x0f,
    //
        0x10,0x11,0x12,0x13,0x3c,0x3d,0x32,0x26, 0x18,0x19,0x3f,0x27,0x1c,0x1d,0x1e,0x1f,
    //  spac !    "    #    $    %    &    '     (    )    *    +    ,    -    .    /
        0x40,0x5a,0x7f,0x7b,0x5b,0x6c,0x50,0x7d, 0x4d,0x5d,0x5c,0x4e,0x6b,0x60,0x4b,0x61,
    //  0    1    2    3    4    5    6    7     8    9    :    ;    <    =    >    ?
        0xf0,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7, 0xf8,0xf9,0x7a,0x5e,0x4c,0x7e,0x6e,0x6f,
    //  @    A    B    C    D    E    F    G     H    I    J    K    L    M    N    O
        0x7c,0xc1,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7, 0xc8,0xc9,0xd1,0xd2,0xd3,0xd4,0xd5,0xd6,
    //  P    Q    R    S    T    U    V    W     X    Y    Z    [    \    ]    &    _
        0xd7,0xd8,0xd9,0xe2,0xe3,0xe4,0xe5,0xe6, 0xe7,0xe8,0xe9,0xba,0xe0,0xbb,0xb0,0x6d,
    //       a    b    c    d    e    f    g     h    i    j    k    l    m    n    o
        0x79,0x81,0x82,0x83,0x84,0x85,0x86,0x87, 0x88,0x89,0x91,0x92,0x93,0x94,0x95,0x96,
    //  p    q    r    s    t    u    v    w     x    y    z    {    |    }    ~
        0x97,0x98,0x99,0xa2,0xa3,0xa4,0xa5,0xa6, 0xa7,0xa8,0xa9,0xc0,0x4f,0xd0,0xa1,0x07,
    };

    for (j = 32; j < 128; j++)                      // look it up in table. (Of course if we constructed
        if (ascii_to_ebcdic_table[j] == ch)         // an ebcdic_to_ascii table we could just get the result directly)
            return j;

    return '?';
}

// -------------------------------------------------------------------------------------------
// convert_namecode - convert two-word name code into 5 character ASCII name
// -------------------------------------------------------------------------------------------

void convert_namecode (uint16 *namecode, char *name)
{
    unsigned long val;
    int i, ch;

    val = (namecode[0] << 16) | namecode[1];    // reconstruct the 30-bit code

    for (i = 0; i < 5; i++) {                   // scan for 5 letters
        ch = ((val >> 24) & 0x3F);              // pick up 6 bits at leftmost character position
        if (ch == 0)
            ch = ' ';                           // zero is a space
        else
            ch = ebcdic_to_ascii(ch | 0xC0);    // add assumed high bits and convert to ASCII
        
        name[i] = ch;                           // save it
        val <<= 6;                              // shift next character into position
    }

    while (--i >= 0)                            // back up to last nonblank character
        if (name[i] != ' ')
            break;      

    name[i+1] = '\0';                           // terminate string
}

// -------------------------------------------------------------------------------------------
// get_let - get FLET or LET, build into linked list
// -------------------------------------------------------------------------------------------

void get_let (LETENTRY **listhead, uint16 secno)
{
    uint16 seq, sec_addr, avail, chain, namecode[2], dbcount, addr;
    int i, nwords, filetype;
    char name[6];
    LETENTRY *head = NULL, *tail, *entry, *master = NULL;

    for (; secno != 0; secno = chain) { // scan through linked sectors
        getsec(secno);

        seq      = sector.data[0];      // relative sector number
        sec_addr = sector.data[1];      // sector address of FA or UA
        avail    = sector.data[3];      // available words in this sector
        chain    = sector.data[4];      // next sector number, 0 if this is the last

        if (seq == 0)                   // first time through, get starting address of FA or UA
            addr = sec_addr*16;         // (convert sector to "disk block")

        if (verbose)
            printf("  (sector %d, addr /%04x, next %04x)\n", seq, secno, chain);

        nwords = SEC_WORDS - 5 - avail; // number of words used by F/LET entries in this sector

        for (i = 5; nwords >= 3; ) {    // scan through entries
            filetype = (sector.data[i] >> 14) & 0x03;       // get file type: 0=DSF, 2=DCI, 3=data

            namecode[0] = sector.data[i] & 0x3FFF;          // get name
            namecode[1] = sector.data[i+1];
            convert_namecode(namecode, name);

            dbcount = sector.data[i+2];                     // get disk block count

            i += 3;                                         // advance index, decrement words-left count
            nwords -= 3;

            entry = ALLOCATE(LETENTRY);
            strcpy(entry->name, name);
            entry->next     = NULL;
            entry->filetype = filetype;
            entry->dbaddr   = addr;
            entry->dbcount  = dbcount;
            entry->dummy    = strcmp(entry->name, "1DUMY") == 0;

            if (dbcount == 0)
                entry->master = master;                     // this is an alternate entry for previous master entry
            else {
                entry->master = NULL;                       // this is a master entry
                master = entry;
            }

            if (head == NULL)
                head = entry;                               // first entry is head of linked list
            else
                tail->next = entry;                         // add to end of linked list

            tail = entry;

            addr += dbcount;                                // skip to next file
        }
    }

    *listhead = head;
}

// -------------------------------------------------------------------------------------------
// print_let - list FLET or LET
// -------------------------------------------------------------------------------------------

void print_let (char *title, LETENTRY *entry)
{
    int nblocks, nfiles, nalternates, nfree;                    // used to get total files, blocks in a directory
    static char *ftname[4] = {"DSF", "???", "DCI", "DDF"};      // filetype names

    nfiles      = 0;                                            // reset total counts
    nblocks     = 0;
    nalternates = 0;
    nfree       = 0;

    printf("%s\n\n", title);
    printf("Name  Type  Blocks%s\n", show_all ? " Addr Type" : "");
    printf("----- ----  ------%s\n", show_all ? " ---- --------------------------------------" : "");

    for (; entry != NULL; entry = entry->next) {
        if (entry->dummy) {                                     // this is an unused LET/FLET slot
            if (entry->next == NULL) {
                nfree = entry->dbcount;                         // last 1DUMY gives amount of free space
            }
            else {
                nblocks += entry-> dbcount;                     // in middle, it's a bit of space lost due to sector-padding

                if (show_all)           {                       // display padding entries in show_all mode
                    printf("%-5s %-3s ", "(pad)", "");
                    commas(entry->dbcount, 8);
                    printf("  %04x\n", entry->dbaddr);
                }
            }
        }
        else if (entry->dbcount > 0) {                          // if disk block count is nonzero, it's a file
            printf("%-5s %-3s", entry->name, ftname[entry->filetype]);
            commas(entry->dbcount, 8);
            if (show_all)
                printf("  %04x %s", entry->dbaddr, file_progtype(entry));
            putchar('\n');

            nblocks += entry->dbcount;                          // add to cumulative totals
            nfiles++;
        }
        else {                                                  // 0 blocks means it's an alternate entry for previous file
            if (show_all)
                printf("%-5s\n", entry->name);

            nalternates++;
        }
    }

    putchar('\n');                                              // double space after table

    printf("\nTotal: ");                                        // print summary
    commas(nfiles, 0);
    printf(" file%s", (nfiles == 1) ? "" : "s");
    if (show_all) {
        printf(", ");
        commas(nalternates, 0);
        printf(" entr%s", (nalternates == 1) ? "y" : "ies");
    }
    putchar('\n');

    printf("Space Used: ");
    commas(nblocks, 0);
    printf(" block%s, ", (nblocks == 1) ? "" : "s");
    commas(nblocks*BLK_WORDS, 0);
    printf(" words\n");

    printf("Space Free: ");
    commas(nfree, 0);
    printf(" block%s, ", (nfree == 1) ? "" : "s");
    commas(nfree*BLK_WORDS, 0);
    printf(" words\n\n");
}

// -------------------------------------------------------------------------------------------
// astring - allocate memory for an return copy of a string
// -------------------------------------------------------------------------------------------

char *astring (char *str)
{
    char *cpy;

    cpy = malloc(strlen(str)+1);
    strcpy(cpy, str);
    
    return cpy;
}

// -------------------------------------------------------------------------------------------
// matchname - see if filename matches (wildcard) file specification. We assume that both
// name and spec are uppercase.
// -------------------------------------------------------------------------------------------

BOOL matchname (char *name, char *spec)
{
    while (*name) {                             // scan through the filename
        if (*name == *spec || *spec == '?') {   // if exact match, or single-char ? match,
            name++;                             // so far so good; skip to next character
            spec++;
        }
        else if (*spec == '*') {                // we are at a * in the pattern. We need to try all possible
            while (*spec == '*')                // see if there are any more literal characters
                spec++;

            if (*spec == '\0')                  // no more literal pattern characters; this qualifies as a match
                return TRUE;

            // if we get here, we need to start matching the remaining part of the pattern against
            // some reduction of the name; we can skip 0 or more characters looking for a hit.
            // For example, if called with matchname("ABCDEF", "AB*F"), when we get here,
            // name = "CDEF" and spec = "F". What we need to do is start eating away at name to see
            // if it can be made to match the final F.

            while (*name) {
                if (matchname(name, spec))      // if the remaining part of the pattern matches the name,
                    return TRUE;                // it's a hit

                name++;                         // skip one character in name (the part matched by our *) and try again
            }

            return FALSE;                       // we skipped everything and still couldn't match the residual pattern
        }
        else
            return FALSE;                       // this is a definite mismatch
    }
                                                // we've hit the end of the actual filename
    while (*spec == '*')
        spec++;                                 // skip over any trailing * wildcards

    return *spec == '\0';                       // match is true if we're now at end of the pattern
}

typedef struct tag_namelist {
    struct tag_namelist *next;
    char name[6];
} NAMENODE, *NAMELIST;

NAMELIST free_nodes = NULL;

#define INDENT  "      "                    // indent for information lines
#define INDENT2 "          "                // indent for debugging lines

// -------------------------------------------------------------------------------------------
// add_list - add a name to a linked list of names, in alphabetical order
// -------------------------------------------------------------------------------------------

void add_list (char *name, NAMELIST *plisthead)
{
    NAMELIST n, prev;
    int cmp;

    for (n = *plisthead, prev = NULL; n != NULL; prev = n, n = n->next) {       // scan list. 'Prev' is trailing pointer
        if ((cmp = strcmp(n->name, name)) == 0)
            return;                                                 // name is already in the list

        if (cmp > 0)                                                // new entry goes before this entry
            break;
    }

    if (free_nodes == NULL)                                         // get a NAMELIST node from freelist or
        n = ALLOCATE(NAMENODE);                                     // by allocating more memory
    else {
        n = free_nodes;
        free_nodes = n->next;
    }

    strcpy(n->name, name);                                          // save the name

    if (prev == NULL) {                                             // add to head of list
        n->next = *plisthead;
        *plisthead = n;
    }
    else {                                                          // add to middle of list, after entry 'prev'
        n->next    = prev->next;
        prev->next = n;
    }
}

// -------------------------------------------------------------------------------------------
// print_list - print list of names
// -------------------------------------------------------------------------------------------

void print_list (NAMELIST list, char *title)
{
    int i;

    printf(INDENT "%-14s", title);                                  // print title string

    for (i = 0; list != NULL; list = list->next, i++) {             // print up to 8 names per line
        if (i == 8) {
            printf("\n" INDENT "%-14s", "");                        // (start a new line)
            i = 0;
        }
        printf("%s%s", (i == 0) ? "" : ", ", list->name);           // print names, separated by commas
    }

    putchar('\n');                                                  // terminate last line
}

// -------------------------------------------------------------------------------------------
// free_list - put list of names into the freelist for possible reuse
// -------------------------------------------------------------------------------------------

void free_list (NAMELIST list)
{
    NAMELIST n;

    if (free_nodes == NULL)                                         // freelist is empty, this list becomes the freelist
        free_nodes = list;
    else {
        for (n = free_nodes; n->next != NULL; n = n->next)          // find last node in freelist
            ;

        n->next = list;                                             // tack this list onto the end
    }
}

// -------------------------------------------------------------------------------------------
// init_dsf_stream - like "fopen", prepares to read data out of a DSF file. A DSFSTREAM structure
// hold necessary state information like file offset, current data module and current data block
// -------------------------------------------------------------------------------------------

typedef struct {
    uint16      dbaddr;                 // dbaddr of current file
    uint16      offset;                 // current offset in file
    uint16      nwords;                 // number of words left in current data module
    uint16      addr;                   // load address of next word
    uint16      nw;                     // number of words in current data block (2-9)
    uint16      ind;                    // index of next word to extract from current data block (1-8)
    uint16      relflag;                // relocation flags; next word's flags are left adjusted
    uint16      datablock[9];           // current data block; datablock[ind] is next word
} DSFSTREAM;

void init_dsf_stream (DSFSTREAM *dsf_stream, LETENTRY *entry, DSF_PROGRAM_HEADER *hdr)
{
    dsf_stream->dbaddr = entry->dbaddr;     // set up dbaddr and offset for data in chosen file
    dsf_stream->offset = hdr->hdr_len9 + 9; // point just past the file header (hdr_len9 is the length - 9)

    dsf_stream->ind    = 999;               // set state so we have to read the first data module
    dsf_stream->nw     = 0;
    dsf_stream->nwords = 0;
}

// -------------------------------------------------------------------------------------------
// get_dsf_word - read next data word and associated relocation flag bits from DSF data stream
// -------------------------------------------------------------------------------------------

BOOL get_dsf_word (DSFSTREAM *dsf_stream, uint16 *word, uint16 *addr, uint16 *relflag)
{
    uint16 dataheader[2];
    int i;

    if (dsf_stream->ind >= dsf_stream->nw) {            // we've exhausted the current data block, get next one
        if (dsf_stream->nwords == 0) {                  // we've exhausted the current module, get next one
            getdata(dataheader, dsf_stream->dbaddr, dsf_stream->offset, 2);     // get two-word data block header
            dsf_stream->offset += 2;

            dsf_stream->addr   = dataheader[0];         // save address and module size
            dsf_stream->nwords = dataheader[1];

            if (dsf_stream->nwords == 0)                // end of file
                return FALSE;

            if (verbose)                                // in verbose mode, show module header
                printf(INDENT2 "%04x %04x %d\n", dsf_stream->addr, dsf_stream->nwords, dsf_stream->nwords-2);

            dsf_stream->nwords -= 2;                    // deduct 2 dataheader words we just read
        }

        dsf_stream->nw = min(dsf_stream->nwords, 9);    // size of next data block
        getdata(dsf_stream->datablock, dsf_stream->dbaddr, dsf_stream->offset, dsf_stream->nw);
        dsf_stream->offset += dsf_stream->nw;           // bump file offset
        dsf_stream->nwords -= dsf_stream->nw;           // and number of words left in current module
        dsf_stream->relflag = dsf_stream->datablock[0]; // get relocation flag word
        dsf_stream->ind     = 1;                        // initialize index

        if (verbose) {                                  // in verbose mode, show data block including relocation bits
            static char flagchar[4] = {'.', 'r', 'L', 'C'};     // (show . r L or C for abs, rel, LIBF or CALL)
            char flagstr[10];

            for (i = 1; i < dsf_stream->nw; i++)                // construct string showing meaning of relocation bits
                flagstr[i-1] = flagchar[(dsf_stream->relflag >> (16-2*i)) & 3];
            flagstr[i-1] = '\0';
                                                                // display address, relocation info, data words
            printf(INDENT2 "   %04x [%04x %-8s]", dsf_stream->addr, dsf_stream->relflag, flagstr);
            for (i = 1; i < dsf_stream->nw; i++)
                printf(" %04x", dsf_stream->datablock[i]);
            putchar('\n');
        }
    }
                                                        // ready to extract the next word...
    *word = dsf_stream->datablock[dsf_stream->ind++];   // give caller the word, and increment index
    *relflag = (dsf_stream->relflag >> 14) & 3;         // give caller the top two bits of the relocation flag word
    dsf_stream->relflag <<= 2;                          // and slide next two bits into place

    *addr = dsf_stream->addr;                           // give caller the word's address, and increment address
    if (*relflag != 2)                                  // unless relflag was 2 (LIBF), which occupies only 1 word
        dsf_stream->addr++;                             // in core. We'll increment addr when we fetch the 2nd name word
}

// -------------------------------------------------------------------------------------------
// print_dsf_info - print information about a Disk System Format file
// -------------------------------------------------------------------------------------------

void print_dsf_info (LETENTRY *entry)
{
    DSF_PROGRAM_HEADER hdr;
    char name[6], *nm, label[4];
    int i, nentries;
    unsigned subtype, progtype, int_precis, real_precis, n_defined_files, fortran_indicator;
    uint16 namewords[2], word, addr, relflag;
    NAMELIST call_list, dsn_list;
    DSFSTREAM dsf_stream;

    getdata(&hdr, entry->dbaddr, 0, sizeof(DSF_PROGRAM_HEADER)/2);      // read file header (assume maximum size)

    subtype           = (hdr.type >> 12) & 0x0F;                        // extract file type and subtype
    progtype          = (hdr.type >>  8) & 0x0F;
    int_precis        = (hdr.type >>  4) & 0x0F;                        // get precision specification
    real_precis       = hdr.type & 0x0F;
    fortran_indicator = (hdr.fortran_info >> 8) & 0xFF;                 // get fortran specifications
    n_defined_files   = hdr.fortran_info & 0xFF;

    if (hdr.zero1 != 0)                                                 // this word is supposed to be zero
        printf(INDENT "CORRUPT:      hdr word 1 should be 0, is %d\n", hdr.zero1);
//  if (hdr.zero2 != 0)                                                 // so is this word, but it turns out not to be reliably zero
//      printf(INDENT "CORRUPT:      hdr word 7 should be 0, is %d\n", hdr.zero2);

    printf(INDENT "Program type: %d=%s\n", progtype, progtype_nm[progtype]);    
    if (progtype == 3 || progtype == 4 || progtype == 5 || progtype == 7) {
        nm = "Undefined";                                               // types 3, 4, 5 and 7 should have a subtype
        for (i = 0; i < N_SUBTYPE_NMS; i++) {
            if (subtype_nm[i].progtype == progtype && subtype_nm[i].subtype == subtype) {
                nm = subtype_nm[i].descr;
                break;
            }
        }
        if (nm != NULL)                                                 // print subtype unless name was defined as NULL
            printf(INDENT "Subtype:      %d=%s\n", subtype, nm);
    }
                                                                        // print fortran information
    printf(INDENT "Precision:    Real=%s Integer=%s\n",
        (real_precis == 0) ? "Unspecified" : (real_precis == 1) ? "Standard"     : (real_precis == 2) ? "Extended" : "invalid",
        (int_precis  == 0) ? "Unspecified" : (int_precis  == 8) ? "Matches Real" : (int_precis  == 9) ? "One word" : "invalid");
    printf(INDENT "Prog length:  %d wd\n", hdr.proglen); 
    printf(INDENT "COMMON:       %d wd\n", hdr.commonlen);
    printf(INDENT "Fortran ind:  0x%02x, %d defined file%s\n",
        fortran_indicator, n_defined_files, (n_defined_files == 1) ? "" : "s");

    switch (progtype) {                                                 // print entry information for...
        default:                                                        // ... mainline or subprogram
            if ((hdr.hdr_len9 % 3) != 0) {
                printf(INDENT "CORRUPT:      header length-9 is %d, should be multiple of 3\n", hdr.hdr_len9);
                break;
            }
            nentries = hdr.hdr_len9 / 3;                                // get number of entry points (assuming not an ILS or ISS)
            if (nentries > 15)
                printf(INDENT "CORRUPT:      # of entries is %d, max is 15\n", nentries);

            for (i = 0; i < nentries; i++) {                            // list entry point names and addresses
                convert_namecode(hdr.x.entry[i].name, name);
                sprintf(label, (i < 9) ? "%d: " : "%d:", i+1);          // print, e.g. "2: " or "12:"
                printf(INDENT "Entry %s     %-5s addr /%04x\n", label, name, hdr.x.entry[i].addr);
            }
            break;

        case 5:                                                         // ... ISS (device interrupt service routine)
        case 6:
            if (hdr.hdr_len9 != 7 && hdr.hdr_len9 != 8)
                printf(INDENT "CORRUPT:      header length-9 is %d, should be 7 or 8\n", hdr.hdr_len9);

            convert_namecode(hdr.x.iss.name, name);                     // has just one entry point name
            printf(INDENT "Entry:        %-5s addr /%04x\n", name, hdr.x.iss.addr);
            printf(INDENT "ISS number:   %d\n", hdr.x.iss.issnumber);

            if (hdr.x.iss.nlevels != 1 && hdr.x.iss.nlevels != 2) {     // should have 1 or 2 associated interrupt levels
                printf(INDENT "CORRUPT:      # of levels is %d, should be 1 or 2\n", hdr.x.iss.nlevels);
                hdr.x.iss.nlevels = 1;
            }

            for (i = 0; i < hdr.x.iss.nlevels; i++)
                printf(INDENT "Int level %d:  %d\n", i+1, hdr.x.iss.level[i]);
            break;

        case 7:                                                         // ... ILS (interrupt handler)
            if (hdr.hdr_len9 != 4)
                printf(INDENT "CORRUPT:      header length-9 is %d, should be 4\n", hdr.hdr_len9);

            convert_namecode(hdr.x.ils.name, name);
            printf(INDENT "Entry:        %-5s addr /%04x\n", name, hdr.x.ils.addr);
            printf(INDENT "ILS level:    %d\n", hdr.x.ils.level);
            break;
    }

    call_list = dsn_list = NULL;                                        // clear list of external references

    init_dsf_stream(&dsf_stream, entry, &hdr);                          // prepare to read data from the file

    while (get_dsf_word(&dsf_stream, &word, &addr, &relflag)) {
        switch (relflag) {
            case 0:                                         // 00 - absolute data
            case 1:                                         // 01 - relocatable data
                break;

            case 2:                                         // 10 - LIBF
                namewords[0] = word;                            // save first name word and get the second
                get_dsf_word(&dsf_stream, &namewords[1], &addr, &relflag);
                convert_namecode(namewords, name);              // convert to ASCII
                add_list(name, &call_list);                     // add name to list of external references
                break;

            case 3:                                         // 11 - CALL or DSN
                namewords[0] = word;                            // save first name word and get the second
                get_dsf_word(&dsf_stream, &namewords[1], &addr, &relflag);
                convert_namecode(namewords, name);              // convert to ASCII
                if (relflag == 0)                               // 1100 - CALL
                    add_list(name, &call_list);                 // add name to list of external references
                else if (relflag == 1)                          // 1101 - DSN
                    add_list(name, &dsn_list);                  // add name to list of data source names
                else                                            // 1110 or 1111 - invalid
                    printf(INDENT, "CORRUPT: object data contains invalid relocation bits 111%d\n", relflag & 1);
                break;
        }
    }

    if (call_list != NULL) {            // print list(s) of external references
        print_list(call_list, "Calls:");
        free_list(call_list);
    }

    if (dsn_list != NULL) {
        print_list(dsn_list, "DSN's referenced:");
        free_list(dsn_list);
    }

    putchar('\n');
}

// -------------------------------------------------------------------------------------------
// print_dci_info - print information about a Disk Core Image file
// -------------------------------------------------------------------------------------------

void print_dci_info (LETENTRY *entry)
{
    DCI_PROGRAM_HEADER hdr;
    char *diskprog;
    int i;

    getdata(&hdr, entry->dbaddr, 0, sizeof(DCI_PROGRAM_HEADER)/2);          // read file header

    diskprog = (hdr.dreq == 0xFFFF) ? "DISKZ" : (hdr.dreq == 0x0000) ? "DISK1" : (hdr.dreq == 0x0001) ? "DISKN" : "Unknown";

    printf(INDENT "Execute addr: /%04x\n", hdr.xeqa);                       // interpret and print it
    printf(INDENT "COMMON:       %d wd\n", hdr.cmon);
    printf(INDENT "Disk IO:      /%04x (%s)\n", hdr.dreq, diskprog);
    printf(INDENT "# files defd: %d\n", hdr.file);
    printf(INDENT "Hdr length:   %d wd\n", hdr.hwct);
    printf(INDENT "Sector cnt:   %d files in WS\n", hdr.lsct);
    printf(INDENT "Load address: /%04x\n", hdr.ldad);
    printf(INDENT "Exit addr:    /%04x\n", hdr.xctl);
    printf(INDENT "TV length:    %d wd\n", hdr.tvwc);
    printf(INDENT "Load size:    %d wd including TV\n", hdr.wcnt-hdr.hwct);
    printf(INDENT "XR3:          /%04x\n", hdr.xr3x);

#define NO_VECTOR 0x0091            // appears to be DMS default handler for unrecognized interrupt

    for (i = 0; i < 6; i++) {
        if (hdr.itv[i] != NO_VECTOR)
            printf(INDENT "Lvl %d vector: /%04x\n", i, hdr.itv[i]);
    }

    if (hdr.ibt[0] != NO_VECTOR || hdr.ibt[1] != NO_VECTOR || hdr.ibt[2] != NO_VECTOR)
        printf(INDENT "ISS of 1231:  /%04x /%04x /%04x\n", hdr.ibt[0], hdr.ibt[1], hdr.ibt[2]);
    if (hdr.ibt[3] != NO_VECTOR)
        printf(INDENT "ISS of 1403:  /%04x\n", hdr.ibt[3]);
    if (hdr.ibt[4] != NO_VECTOR)
        printf(INDENT "ISS of 2501:  /%04x\n", hdr.ibt[4]);
    if (hdr.ibt[5] != NO_VECTOR)
        printf(INDENT "ISS of 1442:  /%04x\n", hdr.ibt[5]);
    if (hdr.ibt[6] != NO_VECTOR)
        printf(INDENT "ISS of kb/pr: /%04x\n", hdr.ibt[6]);
    if (hdr.ibt[7] != NO_VECTOR)
        printf(INDENT "ISS of ptr/p: /%04x\n", hdr.ibt[7]);
    printf(INDENT "LOCAL/SOCALs: %d sectors\n", hdr.ovsw);
    printf(INDENT "Built for:    %d wds core\n", hdr.core);

    putchar('\n');
}

// -------------------------------------------------------------------------------------------
// print_ddf_info - print information about a Disk Data Format file
// -------------------------------------------------------------------------------------------

void print_ddf_info (LETENTRY *entry)
{
    // there's nothing to say, really -- these are user-defined files
}

// -------------------------------------------------------------------------------------------
// dumpfile - print file contents in hex
// -------------------------------------------------------------------------------------------

void dumpfile (LETENTRY *entry)
{
    uint16 offset = 0, nw, nwords, buf[8], i;

    nwords = entry->dbcount*BLK_WORDS;                  // number of words to dump

    while (nwords > 0) {
        printf("   %04x |", offset);                    // print current offset

        nw = min(nwords, 8);                            // fetch (up to) 8 words of data
        getdata(buf, entry->dbaddr, offset, nw);
        offset += nw;                                   // bump offset and count
        nwords -= nw;

        for (i = 0; i < nw; i++)                        // print values in hex
            printf(" %04x", buf[i]);

        putchar('\n');
    }

    putchar('\n');
}

// -------------------------------------------------------------------------------------------
// print_onefile - print detailed info about one particular file
// -------------------------------------------------------------------------------------------

void print_onefile (LETENTRY *entry, BOOL in_flet)
{
    static BOOL first = TRUE;
    LETENTRY *mst;

    if (first) {                                                // print column headings
        first = FALSE;
        printf("Name  Type  Blocks  Addr Remarks\n");
        printf("----- ----  ------  ---- ---------------------------------------------------\n");
    }

    mst = (entry->master != NULL) ? entry->master : entry;      // pointer to main (primary) entry

    printf("%-5s %-3s ", entry->name, ftname[mst->filetype]);   // print file name and size info
    commas(mst->dbcount, 8);
    printf("  %04x", mst->dbaddr);
    if (entry->master != NULL)                                  // this is an alternate entry since it points to a master
        printf(" (alternate entry point in %s)", mst->name);
    printf("%s\n", in_flet ? " (in FLET)" : "");

    if (do_dump)                                                // if -d specified, dump contents
        dumpfile(mst);

    if (show_all) {                                             // if -a specified, print detailed info for particular file type
        switch (mst->filetype) {
            case FILETYPE_DSF:      print_dsf_info(mst);        break;
            case FILETYPE_1:        /* unknown format */        break;
            case FILETYPE_DCI:      print_dci_info(mst);        break;
            case FILETYPE_DDF:      print_ddf_info(mst);        break;
            default:    bail("in print_onefile, can't happen");
        }
    }
}

// -------------------------------------------------------------------------------------------
// list_named_files - print info for file(s) matching a filename specified on the command line
// -------------------------------------------------------------------------------------------

void list_named_files (char *name, char *image)
{
    BOOL has_wild = strchr(name, '?') != NULL || strchr(name, '*') != NULL;
    BOOL in_flet, matched;
    LETENTRY *entry;

    if (flet != NULL) {                             // start at head of FLET if we have one, otherwise LET
        in_flet = TRUE;
        entry   = flet;
    }
    else {
        in_flet = FALSE;
        entry   = let;
    }

    matched = FALSE;

    while (entry != NULL) {                         // scan through flet/let lists
        if (! entry->dummy) {
            if (matchname(entry->name, name)) {     // does this file match the specified name?
                print_onefile(entry, in_flet);      // print it
                matched = TRUE;
                if (! has_wild)                     // if there were no wildcard characters in the name, stop scanning
                    break;
            }
        }
                                                    // try next file...
        if (entry->next == NULL) {                  // if at end of current list
            if (in_flet) {
                entry   = let;                      // move from flet to let
                in_flet = FALSE;
            }
            else
                break;                              // done with both lists
        }
        else
            entry = entry->next;                    // go to next entry in list
    }

    if (! matched)
        printf("%s: no such file in %s", name, image);
}

char * file_progtype (LETENTRY *entry)                  // description of module type
{
    static char buf[100];
    DSF_PROGRAM_HEADER hdr;
    char *nm;
    unsigned subtype, progtype;
    int i;

    *buf = '\0';

    switch (entry->filetype) {
        case FILETYPE_DSF:
            getdata(&hdr, entry->dbaddr, 0, sizeof(DSF_PROGRAM_HEADER)/2);      // read file header (assume maximum size)
            subtype           = (hdr.type >> 12) & 0x0F;                        // extract file type and subtype
            progtype          = (hdr.type >>  8) & 0x0F;

            strcpy(buf, progtype_nm[progtype]); 
            if (progtype == 3 || progtype == 4 || progtype == 5 || progtype == 7) {
                nm = NULL;
                for (i = 0; i < N_SUBTYPE_NMS; i++) {
                    if (subtype_nm[i].progtype == progtype && subtype_nm[i].subtype == subtype) {
                        nm = subtype_nm[i].descr;
                        break;
                    }
                }
                if (nm != NULL) {
                    strcat(buf, "; ");
                    strcat(buf, nm);
                }
            }
            break;

        case FILETYPE_DCI:
            return "Mainline, core image";

        case FILETYPE_DDF:
            return "Data";

        default:
            return "unknown";
    }

    return buf;
}