/* This program dumps the format of a simulated magtape

   Copyright (c) 1993-2022, Robert M. Supnik

   Permission is hereby granted, free of charge, to any person obtaining a
   copy of this software and associated documentation files (the "Software"),
   to deal in the Software without restriction, including without limitation
   the rights to use, copy, modify, merge, publish, distribute, sublicense,
   and/or sell copies of the Software, and to permit persons to whom the
   Software is furnished to do so, subject to the following conditions:

   The above copyright notice and this permission notice shall be included in
   all copies or substantial portions of the Software.

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
   ROBERT M SUPNIK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
   IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
   CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

   Except as contained in this notice, the name of Robert M Supnik shall not
   be used in advertising or otherwise to promote the sale, use or other dealings
   in this Software without prior written authorization from Robert M Supnik.

   26-Apr-22    JDB     Added P7B support
   24-Nov-21    JDB     Added extended SIMH format support
   04-Nov-21    JDB     Now reports bad record length in correct file number
                        Now reports position of invalid record length
   06-Jun-12    JDB     Reformatted; added -a ("all") option
   07-Mar-07    JDB     Fixed precedence error in gap marker test
   30-Aug-06    JDB     Added erase gap support
*/

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <limits.h>

#define F_STD           0                               /* SIMH format */
#define F_E11           1                               /* E11 format */
#define F_TPC           2                               /* TPC format */
#define F_P7B           3                               /* P7B format */
#define F_EXT           4                               /* extended SIMH format */
#define F_TPF           5                               /* TDF format (not implemented) */

typedef unsigned int    uint32;                         /* 32-bit unsigned integer */

/* SIMH/E11 tape format.

    31  30  29  29  27  26  25  24  23  22  21   [...]   2   1   0
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
   | 0   0   0   0   0   0   0   0 | 0   0   0   [...]   0   0   0 | tape mark
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
   | 0 | 0   0   0   0   0   0   0 |          length > 0           | good data record
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
   | 1 | 0   0   0   0   0   0   0 |          length > 0           | bad data record
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
   | 1   1   1   1   1   1   1   1 | 1   1   1   [...]   1   1   0 | erase gap
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
   | 1   1   1   1   1   1   1   1 | 1   1   1   [...]   1   1   1 | end of medium
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

   SIMH data records consist of a leading 4-byte record length, an even number
   of data bytes (padded if necessary), and a trailing 4-byte record length that
   must be the same as the initial record length.

   E11 format is the same as SIMH format with two restrictions:

    1. Record data is not padded to an even number of bytes.

    2. The erase gap marker is not supported.
*/

typedef uint32          t_mtrlnt;                       /* record length type */

#define MTR_TMK         0x00000000                      /* tape mark */
#define MTR_EOM         0xFFFFFFFF                      /* end of medium */
#define MTR_GAP         0xFFFFFFFE                      /* primary gap */
#define MTR_FHGAP       0xFFFEFFFF                      /* forward half gap (overwrite) */
#define MTR_MAXLEN      0x00FFFFFF                      /* max length is 24b */
#define MTR_ERF         0x80000000                      /* error flag */
#define MTR_F(x)        ((x) & MTR_ERF)                 /* record error flag */
#define MTR_L(x)        ((x) & ~MTR_ERF)                /* record length */

/* Extended SIMH tape format.

    31  30  29  29  27  26  25  24  23  22  21   [...]   2   1   0
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
   | control class |             marker-specific value             | marker
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
   | control class |               data length value               | record
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
*/

#define MTR_V_RECLN     0                               /* record length offset */
#define MTR_W_RECLN     28                              /* record length width */
#define MTR_M_RECLN     ((1u << MTR_W_RECLN) - 1)       /* record length mask */
#define MTR_RECLN       (MTR_M_RECLN << MTR_V_RECLN)    /* record length field mask */

#define MTR_V_CLASS     MTR_W_RECLN                     /* class field offset */
#define MTR_W_CLASS     4                               /* class field width */
#define MTR_M_CLASS     ((1u << MTR_W_CLASS) - 1)       /* class number mask */
#define MTR_CLASS       (MTR_M_CLASS << MTR_V_CLASS)    /* class field mask */

#define MTR_RL(m)       ((m) & MTR_RECLN)               /* record length */
#define MTR_CF(m)       ((m) & MTR_CLASS)               /* class field */

#define MTR_NF(cn)      ((t_mtrlnt) (((cn) << MTR_V_CLASS) & MTR_CLASS))    /* class number to class field */
#define MTR_FN(cf)      ((uint32) (((cf) & MTR_CLASS) >> MTR_V_CLASS))      /* class field to class number */

#define MTCN_GOOD       0u                              /* good data record class number */
#define MTCN_PMARK      7u                              /* private marker class number */
#define MTCN_BAD        8u                              /* bad data record class number */
#define MTCN_DESC       14u                             /* standard tape description record class number */
#define MTCN_SMARK      15u                             /* standard marker class number */

#define MTC_GOOD        MTR_NF (MTCN_GOOD)              /* good data record class field */
#define MTC_PMARK       MTR_NF (MTCN_PMARK)             /* private marker class field */
#define MTC_BAD         MTR_NF (MTCN_BAD)               /* bad data record class field */
#define MTC_DESC        MTR_NF (MTCN_DESC)              /* standard tape description record class field */
#define MTC_SMARK       MTR_NF (MTCN_SMARK)             /* standard marker class field */

#define MTR_NB(cn)      (1u << (cn))                    /* class number to class bit */
#define MTR_FB(m)       (MTR_NB (MTR_FN (m)))           /* class field to class bit */

#define MTB_GOOD        MTR_NB (MTCN_GOOD)              /* good data record class bit */
#define MTB_PMARK       MTR_NB (MTCN_PMARK)             /* private marker class bit */
#define MTB_BAD         MTR_NB (MTCN_BAD)               /* bad data record class bit */
#define MTB_DESC        MTR_NB (MTCN_DESC)              /* standard tape description record class bit */
#define MTB_SMARK       MTR_NB (MTCN_SMARK)             /* standard marker class bit */

#define MTB_PRIVREC     (MTR_NB (1) | MTR_NB (2) | \
                         MTR_NB (3) | MTR_NB (4) | \
                         MTR_NB (5) | MTR_NB (6))       /* private record class bits */

#define MTB_RSVDREC     (MTR_NB  (9) | MTR_NB (10) | \
                         MTR_NB (11) | MTR_NB (12) | \
                         MTR_NB (13))                   /* reserved record class bits */

#define MTB_STANDARD    (MTB_GOOD | MTB_BAD)                    /* standard record class bits */
#define MTB_EXTENDED    (MTB_STANDARD | MTB_PRIVREC | MTB_DESC) /* extended record class bits */

#define MTB_MARKERSET   (MTB_PMARK | MTB_SMARK)         /* all marker class bits */
#define MTB_RECORDSET   (~MTB_MARKERSET)                /* all record class bits */

/* TPC tape format.

    15  14  13  12  11  10   9   8   7   6   5   4   3   2   1   0
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
   | 0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0 | tape mark
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
   |                       record length > 0                       | data record
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

   TPC data records consist of a leading 2-byte record length followed by an
   even number of data bytes.
*/

#define TPC_TMK         0x0000                          /* tape mark */

/* P7B tape format.

     7   6   5   4   3   2   1   0
   +---+---+---+---+---+---+---+---+
   | 1 | 0 | 0   0   1   1   1   1 | tape mark
   +---+---+---+---+---+---+---+---+
   | 1 | P | first 6-bit data byte | start of data record
   +---+---+---+---+---+---+---+---+
   | 1 | 0   0   0   0   0   0   0 | dummy last record
   +---+---+---+---+---+---+---+---+

   P7B data records consist 6-bit-plus-parity data bytes.  The first record byte
   has the MSB set to indicate the start of the record,  The remaining bytes
   omit the indicator.  A P7B tape must end with a tape mark or a dummy "last
   record" indicator.  A tape mark is indicated by a character of 217 octal
   (start of record, character 17, and incorrect odd parity or a reserved
   character for even parity).  If a 217 tape mark is followed by additional
   "data" bytes of 017 octal value, the entire sequence is considered to be a
   single tape mark.
*/

#define P7B_SOR         0x80                            /* start of record */
#define P7B_PAR         0x40                            /* parity bit */
#define P7B_TMK         0x8F                            /* eof character */

int main (int argc, char *argv [])
{
int           obj, i, k, fc, rc, tpos;
t_mtrlnt      bc, sbc;
unsigned char *s, *msg, bca [4];
int           preveof, gapcount, gpos, gincr;
FILE          *ifile;
unsigned int  classbit, fmt = F_EXT, rlntsiz = 4, logical = 0;

if ((argc < 2) || (argv [0] == NULL)) {
    printf ("mtdump version 3.1\n\n");
    printf ("Usage: mtdump [OPTION]... FILE...\n");
    printf ("Dump the format of one or more magnetic tape images to stdout.\n\n");
    printf ("Options:\n");
    printf ("  -l   Dump objects until a double tape mark or EOM marker\n");
    printf ("  -s   Dump files in SIMH Standard format\n");
    printf ("  -e   Dump files in E11 format\n");
    printf ("  -c   Dump files in TPC format\n");
    printf ("  -p   Dump files in P7B format\n\n");
    printf ("The default dumps in SIMH Extended format until the physical EOF.\n");

    exit (0);
    }

s = argv [1];                                           /* point at the first argument */

while ((s != NULL) && (*s++ == '-')) {                  /* if this argument is a switch */
    switch (*s) {                                       /*   then dispatch the option letter */

        case 'L':                                       /* logical view */
        case 'l':
            logical = 1;                                /* set a flag to end after successive tape marks */
            break;

        case 'S':                                       /* SIMH standard mode */
        case 's':
            fmt = F_STD;                                /* set the mode */
            rlntsiz = 4;                                /*   and the record length word size */
            break;

        case 'E':                                       /* E11 mode */
        case 'e':
            fmt = F_E11;                                /* set the mode */
            rlntsiz = 4;                                /*   and the record length word size */
            break;

        case 'C':                                       /* TPC mode */
        case 'c':
            fmt = F_TPC;                                /* set the mode */
            rlntsiz = 2;                                /*   and the record length word size */
            break;

        case 'P':                                       /* P7B mode */
        case 'p':
            fmt = F_P7B;                                /* set the mode */
            rlntsiz = 1;                                /*   and the record length word size */
            break;

        default:                                        /* unrecognized switch */
            fprintf (stderr, "Bad option: -%c\n", *s);
            return EXIT_FAILURE;
        }

    --argc;                                             /* drop the argument count */
    ++argv;                                             /*   and point at the next one */

    s = argv [1];                                       /* reset the argument pointer */
    }


for (i = 1; i < argc; i++) {                            /* the remaining arguments are file names */
    if (i > 1)                                          /* if this is not the first file */
        putchar ('\n');                                 /*   then separate from the prior file's output */

    ifile = fopen (argv [i], "rb");                     /* open the file */

    if (ifile == NULL) {                                /* if the open failed */
        printf ("Error opening file: %s\n", argv [i]);  /*   then report it */
        continue;                                       /*     and continue with the next file name */
        }

    printf ("Processing input file %s\n", argv [i]);    /* report the file being processed */

    obj = 1;                                            /* reset the tape object counter */
    rc = 1;                                             /*   and record counter */
    fc = 1;                                             /*     and file counter */

    tpos = 0;                                           /* reset the tape position indicator */
    preveof = 0;                                        /*   and the previous EOF flag */
    gapcount = 0;                                       /*     and erase gap counter */

    printf ("Processing tape file 1\n");                /* report the tape file number */

    if (fmt == F_P7B) {                                 /* if dumping in P7B format */
        bc = 0;                                         /*   then record bytes must be counted */

        do {                                            /* loop through the bytes in the file */
            k = fread (bca, 1, 1, ifile);               /* get the next byte */

            if (k == 0)                                 /* if the read failed */
                if (bc > 1)                             /*   then if in the middle of a record */
                    printf ("Missing end-of-tape indicator\n");
                else                                    /* otherwise */
                    break;                              /*   end the scan */

            else if (bca [0] & P7B_SOR) {               /* otherwise if this is the start of a record */
                if (bc > 0) {                           /*   then if accumulating a prior record */
                    printf ("Obj %d, pos %d (0x%X), record %d, length %d\n",
                            obj, tpos, tpos, rc, bc);   /*     then report it */

                    obj++;                              /* bump the object count */
                    rc++;                               /*   and record count */
                    tpos = tpos + bc;                   /*     and update the tape position */
                    }

                if (bca [0] == P7B_TMK) {               /* if this is a tape mark */
                    if (preveof)                        /*   then if there was a preceding tape mark */
                        if (logical) {                  /*     then if logical view is enabled */
                            printf ("Obj %d, pos %d (0x%X), end of logical tape\n",
                                    obj, tpos, tpos);
                            break;                      /*       then end the scan */
                            }

                        else                            /* otherwise the new mark starts and ends a new file */
                            printf ("Processing tape file %d\n", fc);

                    preveof = 1;                        /* set the prior tape mark flag */

                    printf ("Obj %d, pos %d (0x%X), end of tape file %d\n",
                            obj, tpos, tpos, fc);

                    obj++;                              /* bump the object count */
                    fc++;                               /*   and file count */

                    rc = 1;                             /* reset the record counter */
                    bc = 0;                             /*   and the byte counter */
                    tpos = tpos + 1;                    /*     and update the tape position */
                    }

                else {                                  /* otherwise it's the start of a data record */
                    if (preveof) {                      /* if preceded by a tape mark */
                        printf ("Processing tape file %d\n", fc);
                        preveof = 0;                    /*   then report the new file and clear the flag */
                        }

                    bc = 1;                             /* set the counter for the leading byte */
                    }
                }

            else if (bc > 0)                            /* otherwise if we're within a data record */
                bc++;                                   /*   then count the byte */

            else if (bca [0] == (P7B_TMK & ~P7B_SOR))   /* otherwise if it's an embedded tape mark */
                tpos++;                                 /*   then update the tape position */

            else {                                      /* otherwise the required start-of-record */
                printf ("Missing start-of-record indicator\nTerminating dump\n");
                break;                                  /*   indicator is missing */
                }
            }
        while (k == 1);                                 /* continue as long as reads are successful */
        }


    else do {                                           /* otherwise this is SIMH/E11/TPC format */
        fseek (ifile, tpos, SEEK_SET);                  /* seek to the current tape position */
        k = fread (bca, 1, rlntsiz, ifile);             /*   and read a metadatum marker */

        switch (fmt) {                                  /* dispatch on the tape format */
            case F_STD:                                 /* SIMH standard format */
            case F_EXT:                                 /* SIMH extended format */
            case F_E11:                                 /* E11 format */
                bc = (unsigned int) bca [0]             /* assemble the record length */
                     | (((unsigned int) bca [1]) << 8)  /*   from four bytes */
                     | (((unsigned int) bca [2]) << 16) /*     in little-endian order */
                     | (((unsigned int) bca [3]) << 24);
                break;

            case F_TPC:                                 /* TPC format */
                bc = (unsigned int) bca [0]             /* assemble the record length */
                     | (((unsigned int) bca [1]) << 8); /*   from two bytes */
                break;                                  /*     in little-endian order */
            }

        if (k == rlntsiz                                /* if the read succeeded */
          && ((bc == MTR_GAP) || (bc == MTR_FHGAP))) {  /*   and read a gap marker */
            if (gapcount == 0)                          /*     then if a new gap is starting */
                gpos = tpos;                            /*       then save the starting position */

            if (bc == MTR_FHGAP)                        /* if a half-gap marker was read */
                gincr = 2;                              /*   then it occupies two bytes */
            else                                        /* otherwise a full-gap marker */
                gincr = 4;                              /*   occupies four bytes */

            gapcount = gapcount + gincr;                /* update the gap count */
            tpos = tpos + gincr;                        /*   and the tape position */
            continue;                                   /*     and continue */
            }

        else if (gapcount) {                            /* otherwise if a gap is ending */
            printf ("Obj %d, pos %d (0x%X), erase gap, length %d (0x%X)\n",
                    obj, gpos, gpos, gapcount, gapcount);
            obj++;                                      /* then bump the object count */
            gapcount = 0;                               /*   and reset the gap counter */
            }


        if (k < rlntsiz)                                /* if the read failed */
            break;                                      /*   then end the scan */

        else if (bc == MTR_EOM) {                       /* otherwise if this is an EOM */
            if (logical) {                              /*   then if in logical view */
                printf ("End of medium\n");             /*     then report it */
                break;                                  /*       and end the scan */
                }

            else                                        /* otherwise report it and continue */
                printf ("Obj %d, pos %d (0x%X), end of medium\n",
                        obj, tpos, tpos);

            obj++;                                      /* bump the object count */
            fc++;                                       /*   and file count */

            rc = 1;                                     /* reset the record counter */
            tpos = tpos + rlntsiz;                      /*   and update the tape position */
            }

        else if (bc == MTR_TMK) {                       /* otherwise if this is a tape mark */
            if (preveof)                                /*   then if there was a preceding tape mark */
                if (logical) {                          /*     then if logical view is enabled */
                    printf ("Obj %d, pos %d (0x%X), end of logical tape\n",
                            obj, tpos, tpos);
                    break;                              /*       then end the scan */
                    }

                else                                    /* otherwise the mark starts and ends a new file */
                    printf ("Processing tape file %d\n", fc);

            preveof = 1;                                /* set the prior tape mark flag */

            printf ("Obj %d, pos %d (0x%X), end of tape file %d\n",
                    obj, tpos, tpos, fc);

            obj++;                                      /* bump the object count */
            fc++;                                       /*   and file count */

            rc = 1;                                     /* reset the record counter */
            tpos = tpos + rlntsiz;                      /*   and update the tape position */
            }

        else {                                          /* otherwise */
            if (preveof) {                              /*   if preceded by a tape mark */
                printf ("Processing tape file %d\n", fc);
                preveof = 0;                            /*     then report the new file and clear the flag */
                }

            if (fmt == F_EXT) {                         /* if the format is SIMH extended */
                sbc      = MTR_RL (bc);                 /*   then get the record length */
                classbit = MTR_FB (bc);                 /*     and the bit associated with the class */

                if (classbit == MTB_PMARK) {            /* if it's a private marker class */
                    printf ("Obj %d, pos %d (0x%X), private marker %d (0x%X)\n",
                            obj, tpos, tpos, sbc, bc);

                    obj++;                              /* bump the object count */
                    tpos = tpos + rlntsiz;              /*   and update the tape position */
                    continue;                           /*     and continue the dump */
                    }

                else if (classbit == MTB_SMARK) {       /* otherwise if it's a standard marker class */
                    printf ("Obj %d, pos %d (0x%X), reserved marker %d (0x%X)\n",
                            obj, tpos, tpos, sbc, bc);

                    obj++;                              /* bump the object count */
                    tpos = tpos + rlntsiz;              /*   and update the tape position */
                    continue;                           /*     and continue the dump */
                    }

                else if (classbit == MTB_GOOD)          /* otherwise if it's a good data record */
                    msg = "data";                       /*   then use the "data" tag */

                else if (classbit == MTB_BAD)           /* otherwise if it's a bad record */
                    msg = "bad";                        /*   then use the "bad" tag */

                else if (classbit == MTB_DESC)          /* otherwise if it's a description record */
                    msg = "descriptive";                /*   then tag it */

                else if (classbit & MTB_PRIVREC)        /* otherwise if it's a private record */
                    msg = "private";                    /*   then tag it */

                else                                    /* otherwise */
                    msg = "reserved";                   /*   it's a reserved record */

                if (classbit & (MTB_STANDARD | MTB_DESC))
                    printf ("Obj %d, pos %d (0x%X), %s record %d, length %d (0x%X)\n",
                            obj, tpos, tpos, msg, rc, sbc, sbc);
                else
                    printf ("Obj %d, pos %d (0x%X), %s class %d record %d, length %d (0x%X)\n",
                            obj, tpos, tpos, msg, MTR_FN (bc), rc, sbc, sbc);

                sbc = (sbc + 1) & ~1;                   /* round the byte count to an even number */
                }

            else {                                      /* otherwise it's SIMH standard or E11 format */
                sbc = MTR_L (bc);                       /*   so get the record length */

                printf ("Obj %d, pos %d (0x%X), record %d, length %d (0x%X)",
                        obj, tpos, tpos, rc, sbc, bc);

                if (sbc > MTR_MAXLEN) {                 /* if the length is illegal */
                    printf (", length error\nTerminating dump\n");
                    break;                              /*   then end the scan */
                    }

                else if (MTR_F (bc))                    /* otherwise if the record is bad */
                    printf (", data error\n");          /*   then report the data error */

                else                                    /* otherwise */
                    printf ("\n");                      /*   it's a good record */

                if (fmt != F_E11)                       /* if the format is not E11 */
                    sbc = (sbc + 1) & ~1;               /*   then round the byte count to an even number */

                if (fmt == F_TPC) {                     /* if the format is TPC */
                    tpos += rlntsiz + sbc;              /*   then update the tape position */
                    continue;                           /*     and continue the dump */
                    }
                }

            tpos += rlntsiz + sbc;                      /* update the tape position */

            fseek (ifile, tpos, SEEK_SET);              /* seek to the end of the data area */
            k = fread (&sbc, 1, rlntsiz, ifile);        /*   and read the trailing marker */

            if (k < rlntsiz) {                          /* if the read failed */
                clearerr (ifile);                       /*   then clear the error */

                fseek (ifile, 0, SEEK_END);             /* seek to the end of the file */
                tpos = ftell (ifile);                   /*   and get the EOF position */

                printf ("Obj %d, pos %d (0x%X), file ends before trailing record length\n",
                        obj, tpos, tpos);
                }

            else if (bc != sbc)                         /* otherwise if the two markers don't match */
                printf ("Obj %d, pos %d (0x%X), trailing length 0x%X does not match 0x%X\n",
                        obj, tpos, tpos, sbc, bc);

            obj++;                                      /* bump the object count */
            rc++;                                       /*   and record count */
            tpos = tpos + rlntsiz;                      /*     and update the tape position */
            }
        }
    while (k == rlntsiz);                               /* continue as long as reads are successful */

    if (k < rlntsiz)                                    /* if a read failed */
        printf ("End of physical tape\n");              /*   then report hitting the EOF */

    fclose (ifile);                                     /* close the file */
    }                                                   /*   and continue with the next one */

return EXIT_SUCCESS;                                    /* return success */
}
