/* sim_tape.c: simulator tape support library

   Copyright (c) 1993-2023, 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.

   Ultimately, this will be a place to hide processing of various tape formats,
   as well as OS-specific direct hardware access.

   07-Feb-23    RMS     Silenced Mac compiler warnings (Ken Rector)
   15-Dec-21    JDB     Added extended SIMH format support
   10-Oct-21    JDB     Improved tape_erase_fwd corrupt image error checking
   06-Oct-21    JDB     Added sim_tape_erase global, tape_erase internal functions
   27-Dec-18    JDB     Added missing fall through comment in sim_tape_wrrecf
   03-May-17    JDB     Added support for erasing tape marks to sim_tape_errec[fr]
   02-May-17    JDB     Added error checks to sim_fseek calls
   18-Jul-16    JDB     Added sim_tape_errecf, sim_tape_errecr functions
   12-Oct-15    JDB     Fixed bug in sim_tape_rdlntf if gap buffer read ends at EOM
   15-Dec-14    JDB     Changed sim_tape_set_dens to check validity of density change
   04-Nov-14    JDB     Restrict sim_tape_set_fmt to unit unattached
   31-Oct-14    JDB     Fixed gap skip on reverse read
                        Fixed write EOM bug (should not update position)
                        Added set/show density functions, changed sim_tape_wrgap API
                        Buffered forward/reverse gap skip to improve execution time
   22-Sep-14    JDB     Added tape runaway support
   08-Jun-08    JDB     Fixed signed/unsigned warning in sim_tape_set_fmt
   23-Jan-07    JDB     Fixed backspace over gap at BOT
   22-Jan-07    RMS     Fixed bug in P7B format read reclnt rev (found by Rich Cornwell)
   15-Dec-06    RMS     Added support for small capacity tapes
   30-Aug-06    JDB     Added erase gap support
   14-Feb-06    RMS     Added variable tape capacity
   23-Jan-06    JDB     Fixed odd-byte-write problem in sim_tape_wrrecf
   17-Dec-05    RMS     Added write support for Paul Pierce 7b format
   16-Aug-05    RMS     Fixed C++ declaration and cast problems
   02-May-05    RMS     Added support for Pierce 7b format
   28-Jul-04    RMS     Fixed bug in writing error records (found by Dave Bryan)
                RMS     Fixed incorrect error codes (found by Dave Bryan)
   05-Jan-04    RMS     Revised for file I/O library
   25-Apr-03    RMS     Added extended file support
   28-Mar-03    RMS     Added E11 and TPC format support


   The tape formats and functions implemented herein are described in separate
   monographs titled, "SIMH Magtape Representation and Handling" and "Writing a
   Simulator for the SIMH System," respectively.


   Public routines:

   sim_tape_attach      attach tape unit
   sim_tape_detach      detach tape unit
   sim_tape_rdrecf      read tape record forward
   sim_tape_rdrecr      read tape record reverse
   sim_tape_wrrecf      write tape record forward
   sim_tape_sprecf      space tape record forward
   sim_tape_sprecr      space tape record reverse
   sim_tape_wrmrk       write private marker
   sim_tape_wrtmk       write tape mark
   sim_tape_wreom       erase remainder of tape
   sim_tape_wrgap       write erase gap
   sim_tape_errecf      erase record forward
   sim_tape_errecr      erase record reverse
   sim_tape_erase       erase a specified number of bytes
   sim_tape_rewind      rewind
   sim_tape_reset       reset unit
   sim_tape_bot         TRUE if at beginning of tape
   sim_tape_eot         TRUE if at or beyond end of tape
   sim_tape_wrp         TRUE if write protected
   sim_tape_set_fmt     set tape format
   sim_tape_show_fmt    show tape format
   sim_tape_set_capac   set tape capacity
   sim_tape_show_capac  show tape capacity
   sim_tape_set_dens    set tape density
   sim_tape_show_dens   show tape density
*/

#include "sim_defs.h"
#include "sim_tape.h"

struct sim_tape_fmt {
    char                *name;                          /* name */
    int32               uflags;                         /* unit flags */
    t_addr              bot;                            /* bot test */
    };

static const struct sim_tape_fmt fmts [] = {            /* format table, indexed by MTUF_F number */
/*     name          uflags                  bot          */
/*    -------  -------------------  --------------------- */
    { "SIMH",  MT_F_STD,            sizeof (t_mtrlnt) - 1 },    /* 0 = MTUF_F_STD */
    { "E11",   MT_F_E11,            sizeof (t_mtrlnt) - 1 },    /* 1 = MTUF_F_E11 */
    { "TPC",   MT_F_TPC | UNIT_RO,  sizeof (t_tpclnt) - 1 },    /* 2 = MTUF_F_TPC */
    { "P7B",   MT_F_P7B,            0                     },    /* 3 = MTUF_F_P7B */
    { "   ",   MT_F_TDF | UNIT_RO,  0                     },    /* 4 = MTUF_F_TDF (not implemented) */
    { "SIMH",  MT_F_EXT,            sizeof (t_mtrlnt) - 1 }     /* 5 = MTUF_F_EXT */
    };

#define FMT_COUNT       (sizeof fmts / sizeof fmts [0]) /* count of format table entries */

static const uint32 bpi [] = {                          /* tape density table, indexed by MT_DENS constants */
    0,                                                  /*   0 = MT_DENS_NONE -- density not set */
    200,                                                /*   1 = MT_DENS_200  -- 200 bpi NRZI */
    556,                                                /*   2 = MT_DENS_556  -- 556 bpi NRZI */
    800,                                                /*   3 = MT_DENS_800  -- 800 bpi NRZI */
    1600,                                               /*   4 = MT_DENS_1600 -- 1600 bpi PE */
    6250                                                /*   5 = MT_DENS_6250 -- 6250 bpi GCR */
    };

#define BPI_COUNT       (sizeof (bpi) / sizeof (bpi [0]))   /* count of density table entries */

static t_stat sim_tape_ioerr (UNIT *uptr);
static t_stat sim_tape_wrdata (UNIT *uptr, uint32 dat);
static uint32 sim_tape_tpc_map (UNIT *uptr, t_addr *map);
static t_addr sim_tape_tpc_fnd (UNIT *uptr, t_addr *map);
static t_stat tape_read      (UNIT *uptr, uint8 *buffer, t_mtrlnt *class_count, t_mtrlnt bufsize, t_bool reverse);
static t_stat tape_erase     (UNIT *uptr, t_mtrlnt byte_count);
static t_stat tape_erase_fwd (UNIT *uptr, t_mtrlnt gap_size);
static t_stat tape_erase_rev (UNIT *uptr, t_mtrlnt gap_size);


/* Attach tape unit */

t_stat sim_tape_attach (UNIT *uptr, char *cptr)
{
uint32 objc;
char gbuf[CBUFSIZE];
t_stat r;

if (sim_switches & SWMASK ('F')) {                      /* format spec? */
    cptr = get_glyph (cptr, gbuf, 0);                   /* get spec */
    if (*cptr == 0)                                     /* must be more */
        return SCPE_2FARG;
    if (sim_tape_set_fmt (uptr, 0, gbuf, NULL) != SCPE_OK)
        return SCPE_ARG;
    }
r = attach_unit (uptr, cptr);                           /* attach unit */
if (r != SCPE_OK)                                       /* error? */
    return r;
switch (MT_GET_FMT (uptr)) {                            /* case on format */

    case MTUF_F_TPC:                                    /* TPC */
        objc = sim_tape_tpc_map (uptr, NULL);           /* get # objects */
        if (objc == 0) {                                /* tape empty? */
            sim_tape_detach (uptr);
            return SCPE_FMT;                            /* yes, complain */
            }
        uptr->filebuf = calloc (objc + 1, sizeof (t_addr));
        if (uptr->filebuf == NULL) {                    /* map allocated? */
            sim_tape_detach (uptr);
            return SCPE_MEM;                            /* no, complain */
            }
        uptr->hwmark = objc + 1;                        /* save map size */
        sim_tape_tpc_map (uptr, (t_addr *) uptr->filebuf);      /* fill map */
        break;

    default:
        break;
    }

sim_tape_rewind (uptr);
return SCPE_OK;
}

/* Detach tape unit */

t_stat sim_tape_detach (UNIT *uptr)
{
uint32 f = MT_GET_FMT (uptr);
t_stat r;

r = detach_unit (uptr);                                 /* detach unit */
if (r != SCPE_OK)
    return r;
switch (f) {                                            /* case on format */

    case MTUF_F_TPC:                                    /* TPC */
        if (uptr->filebuf)                              /* free map */
            free (uptr->filebuf);
        uptr->filebuf = NULL;
        uptr->hwmark = 0;
        break;

    default:
        break;
        }

sim_tape_rewind (uptr);
return SCPE_OK;
}


/* Read record length forward (internal routine).

   Inputs:
        uptr    =       pointer to tape unit
        bc      =       pointer to returned record length

   Outputs:
        status  =       operation status

   exit condition       tape position
   ------------------   -----------------------------------------------------
   unit unattached      unchanged
   read error           unchanged, PNU set if initial read
   end of file/medium   updated if a gap precedes, else unchanged and PNU set
   tape mark            updated
   other marker         updated
   tape runaway         updated
   data record          updated, sim_fread will read record forward

   This routine is called to set up a record read or spacing in the forward
   direction.  On return, status is MTSE_OK if a data record, private marker, or
   reserved marker was read, or an MTSE error code if a standard marker (e.g.
   tape mark) was read, or an error occurred.  The file is positioned at the
   first byte of a data record, after a tape mark or private marker, or
   otherwise as indicated above.  In all cases, the successfully read marker or
   data record length word is returned via the "bc" pointer.

   When the extended SIMH format is enabled, then the variable addressed by the
   "bc" parameter must be set on entry to a bitmap of the object classes to
   return.  Each of the classes is represented by its corresponding bit, i.e.,
   bit 0 represents class 0, bit 1 for class 1, etc.  The routine will return
   only objects from the selected classes.  Unselected class objects will be
   ignored by skipping over them until the first selected class object is seen.
   This allows a simulator to declare those classes it understands (e.g.,
   standard classes 0 and 8, plus private classes 2 and 7) and those classes it
   wishes to ignore.  Erase gap markers are always skipped, and standard markers
   are always returned, so specifying an empty bitmap will perform the
   equivalent of a "space file forward," returning only when a tape mark or
   EOM/EOF is encountered.

   When standard SIMH format is enabled, standard classes 0 and 8 are
   automatically selected, and the entry value addressed by "bc" is ignored.

   The ANSI standards for magnetic tape recording (X3.22, X3.39, and X3.54) and
   the equivalent ECMA standard (ECMA-62) specify a maximum erase gap length of
   25 feet (7.6 meters).  While gaps of any length may be written, gaps longer
   than this are non-standard and may indicate that an unrecorded or erased tape
   is being read.

   If the tape density has been set via a previous "sim_tape_set_dens" call,
   then the length is monitored when skipping over erase gaps.  If the length
   reaches 25 feet, motion is terminated, and MTSE_RUNAWAY status is returned.
   Runaway status is also returned if an end-of-medium marker or the physical
   end of file is encountered while spacing over a gap; however, MTSE_EOM is
   returned if the tape is positioned at the EOM or EOF on entry.

   If the density has not been set, then a gap of any length is skipped, and
   MTSE_RUNAWAY status is never returned.  In effect, erase gaps present in the
   tape image file will be transparent to the caller.

   Erase gaps are currently supported only in standard and extended SIMH tape
   formats.  Because gaps may be partially overwritten with data records, gap
   metadata must be examined marker-by-marker.  To reduce the number of file
   read calls, a buffer of metadata elements is used.  The buffer size is
   initially established at 256 elements but may be set to any size desired.  To
   avoid a large read for the typical case where an erase gap is not present,
   the first read is of a single metadatum marker.  If that is a gap marker,
   then additional buffered reads are performed.

   The permissibility of data record lengths that are not multiples of the
   metadatum size presents a difficulty when reading through gaps.  If such an
   "odd length" record is written over a gap, half of a gap marker will exist
   immediately after the trailing record length.

   This condition is detected when reading forward by the appearance of a
   "reversed" marker.  The value appears reversed because the value is made up
   of half of one marker and half of the next.  This is handled by seeking
   forward two bytes to resync (it is illegal to overwrite and leave only two
   bytes of gap, so at least one "whole" metadata marker will follow the
   half-gap).


   Implementation notes:

    1. For programming convenience, erase gap processing is performed for both
       SIMH and E11 tape formats, although the latter will never contain erase
       gaps, as the "tape_erase_fwd" call takes no action for the E11 format.

    2. The "runaway_counter" cannot decrement to zero (or below) in the presence
       of an error that terminates the gap-search loop.  Therefore, the test
       after the loop exit need not check for error status, except to check
       whether an EOM occurred while reading a gap.

    3. The dynamic start/stop test of the HP 3000 magnetic tape diagnostic
       heavily exercises the erase gap scanning code.  Sample test execution
       times for various buffer sizes on a 2 GHz host platform are:

         buffer size    execution time
         (elements)     (CPU seconds)
         -----------    --------------
               1             7200
              32              783
             128              237
             256              203
             512              186
            1024              171

    4. Because an erase gap may precede the logical end-of-medium, represented
       either by the physical end-of-file or by an EOM marker, the "position not
       updated" flag is set only if the tape is positioned at the EOM when the
       routine is entered.  If at least one gap marker precedes the EOM, then
       the PNU flag is not set.  This ensures that a backspace-and-retry
       sequence will work correctly in both cases.

    5. When a data record length word is seen, a check is made to see if the
       word is the last word in the metadata buffer.  If it is, then the file
       stream is correctly positioned to read the data, i.e., is positioned
       immediately after the length word.  If the word is somewhere within the
       buffer, then the stream is repositioned to the location of the start of
       the data.

    6. A skipped data record may reside entirely within the metadata buffer.
       However, the buffer consists of four-byte elements, and a data record may
       not end on an element boundary.  Rather than testing for this and
       succeeding only half of the time, we unilaterally reposition the file
       stream and invalidate the buffer to force a read.

    7. A partial buffer read without a host I/O error occurs when the physical
       EOF is reached.  If the buffer contains only erase gap markers, i.e., the
       tape image ends with a gap, then the next read will return zero because
       the EOF flag is set.  In this case, we could avoid this read by calling
       the "feof" function first.  We do not, because the common case -- entry
       with the file positioned at EOF -- will not have the EOF flag set, as the
       preceding "fseek" resets it, so the read call would be made anyway.
       Thus, we would incur a small overhead on every call to save some overhead
       on a rare corner-case.
*/

static t_stat sim_tape_rdlntf (UNIT *uptr, t_mtrlnt *bc)
{
const uint32 f = MT_GET_FMT (uptr);                     /* the tape format */
uint8        c;
t_bool       all_eof;
t_mtrlnt     sbc;
t_tpclnt     tpcbc;
t_mtrlnt     buffer [256];                              /* local tape buffer */
uint32       bufcntr, bufcap;                           /* buffer counter and capacity */
uint32       classbit;                                  /* bit representing the object class */
int32        runaway_counter, max_gap, sizeof_gap;      /* bytes remaining before runaway and bytes per gap */
t_addr       next_pos;                                  /* next record position */
t_stat       status = MTSE_OK;                          /* preset status return */
uint32       accept = MTB_STANDARD;                     /* preset the standard class acceptance set */

MT_CLR_PNU (uptr);                                      /* clear the position-not-updated flag */

if ((uptr->flags & UNIT_ATT) == 0)                      /* if the unit is not attached */
    return MTSE_UNATT;                                  /*   then quit with an error */

if (sim_fseek (uptr->fileref, uptr->pos, SEEK_SET)) {   /* set the initial tape position; if it fails */
    MT_SET_PNU (uptr);                                  /*   then set position not updated */
    status = sim_tape_ioerr (uptr);                     /*     and quit with I/O error status */
    }

else switch (f) {                                       /* otherwise the read method depends on the tape format */

    case MTUF_F_EXT:
        accept = (uint32) (*bc);                        /* get the set of acceptable classes */

    /* fall through into the standard SIMH and E11 handler */

    case MTUF_F_STD:
    case MTUF_F_E11:
        max_gap = 25 * 12                               /* set the largest legal gap size in bytes */
                    * bpi [MT_DENS (uptr->dynflags)];   /*   corresponding to 25 feet of tape */

        if (max_gap == 0) {                             /* if tape density has not been not set */
            sizeof_gap = 0;                             /*   then disable runaway detection */
            max_gap = INT_MAX;                          /*     and allow gaps of any size */
            }

        else                                            /* otherwise */
            sizeof_gap = sizeof (t_mtrlnt);             /*   set the size of the gap */

        runaway_counter = max_gap;                      /* initialize the runaway counter */

        bufcntr = 0;                                    /* force an initial read */
        bufcap = 0;                                     /*   but of just one metadata marker */

        do {                                            /* loop until an object is accepted or an error occurs */
            if (bufcntr == bufcap) {                    /* if the buffer is empty */
                if (bufcap == 0)                        /*   then if this is the initial read */
                    bufcap = 1;                         /*     then start with just one marker */
                else                                    /*   otherwise reset the capacity */
                    bufcap = sizeof (buffer)            /*     to the full size of the buffer */
                               / sizeof (buffer [0]);

                bufcap = sim_fread (buffer, sizeof (t_mtrlnt),  /* fill the buffer */
                                    bufcap, uptr->fileref);     /*   with tape metadata */

                if (ferror (uptr->fileref)) {           /* if a file I/O error occurred */
                    if (bufcntr == 0)                   /*   then if this is the initial read */
                        MT_SET_PNU (uptr);              /*     then set position-not-updated */

                    status = sim_tape_ioerr (uptr);     /* report the error and quit */
                    break;
                    }

                else if (bufcap == 0                    /* otherwise if positioned at the physical EOF */
                  || buffer [0] == MTR_EOM) {           /*   or at the logical EOM */
                    if (bufcntr == 0)                   /*     then if this is the initial read */
                        MT_SET_PNU (uptr);              /*       then set position not updated */

                    if (bufcap == 0)                    /* if an EOM marker was not read */
                        *bc = 0;                        /*   then zero the marker value */
                    else                                /* otherwise */
                        *bc = MTR_EOM;                  /*   store the EOM value */

                    status = MTSE_EOM;                  /* report the end-of-medium */
                    break;                              /*   and quit */
                    }

                else                                    /* otherwise reset the index */
                    bufcntr = 0;                        /*   to the start of the buffer */
                }

            *bc = buffer [bufcntr++];                   /* store the metadata marker value */

            if (*bc == MTR_EOM) {                       /* if an end-of-medium marker is seen */
                status = MTSE_EOM;                      /*   then report the end-of-medium */
                break;                                  /*     and quit */
                }

            uptr->pos = uptr->pos + sizeof (t_mtrlnt);  /* space over the marker */

            if (*bc == MTR_TMK) {                       /* if the marker is a tape mark */
                status = MTSE_TMK;                      /*   then quit with tape mark status */
                break;
                }

            else if (*bc == MTR_GAP)                    /* otherwise if the marker is a full gap */
                runaway_counter -= sizeof_gap;          /*   then decrement the gap counter */

            else if (*bc == MTR_FHGAP) {                        /* otherwise if the marker if a half gap */
                uptr->pos = uptr->pos - sizeof (t_mtrlnt) / 2;  /*   then back up to resync */

                if (sim_fseek (uptr->fileref, uptr->pos, SEEK_SET)) {   /* set the tape position; if it fails */
                    status = sim_tape_ioerr (uptr);                     /*   then quit with I/O error status */
                    break;
                    }

                bufcntr = bufcap;                       /* mark the buffer as invalid to force a read */
                runaway_counter -= sizeof_gap / 2;      /*   and decrement the gap counter by half */
                }

            else {                                      /* otherwise it is not a known marker */
                classbit = MTR_FB (*bc);                /*   so get the bit corresponding to the class */

                next_pos = uptr->pos + MTR_RL (*bc)     /* it it's a data record */
                             + sizeof (t_mtrlnt);       /*   then preset to the end of the record */

                if (f != MTUF_F_E11)                    /* if the format is not E11 */
                    next_pos += MTR_RL (*bc) & 1;       /*   then record sizes are an even number of bytes */

                if (classbit & accept) {                /* if the class is accepted */
                    if (classbit == MTB_SMARK)          /*   then if it's a SIMH-reserved marker */
                        status = MTSE_RESERVED;         /*     then return reserved status */

                    else if (classbit == MTB_PMARK)     /*   otherwise if it's a private marker */
                        status = MTSE_OK;               /*     then return successful status */

                    else if (bufcntr == bufcap                  /*   otherwise if the record starts after the buffer */
                      || sim_fseek (uptr->fileref,              /*     or repositioning to the start */
                                    uptr->pos, SEEK_SET) == 0)  /*       of the data area succeeds */
                        uptr->pos = next_pos;                   /*         then position past the record */

                    else                                /*   otherwise the seek failed */
                        status = sim_tape_ioerr (uptr); /*     so quit with I/O error status */

                    break;                              /* acceptance terminates the search */
                    }

                else if (classbit & MTB_RECORDSET) {    /* otherwise if ignoring a data record */
                    uptr->pos = next_pos;               /*   then position past the record */

                    if (sim_fseek (uptr->fileref, uptr->pos, SEEK_SET)) {   /* set the new position; if it fails */
                        status = sim_tape_ioerr (uptr);                     /*   then quit with I/O error status */
                        break;
                        }

                    bufcntr = bufcap;                   /* mark the buffer as invalid to force a read */
                    }

                runaway_counter = max_gap;              /* ignoring a marker or record resets the counter */
                }
            }
        while (runaway_counter > 0);                    /* continue searching until runaway occurs */

        if (sizeof_gap > 0                              /* if gap detection is enabled */
          && (runaway_counter <= 0                      /*   and a tape runaway occurred */
          || (status == MTSE_EOM                         /*   or EOM/EOF was seen */
          && runaway_counter < max_gap)))                /*     while a gap was being skipped */
            status = MTSE_RUNAWAY;                      /*       then report it */

        break;                                          /* end of case */


    case MTUF_F_TPC:
        sim_fread (&tpcbc, sizeof (t_tpclnt), 1, uptr->fileref);
        *bc = tpcbc;                                    /* save rec lnt */

        if (ferror (uptr->fileref)) {                   /* error? */
            MT_SET_PNU (uptr);                          /* pos not upd */
            status = sim_tape_ioerr (uptr);
            }
        else if (feof (uptr->fileref)) {                /* eof? */
            MT_SET_PNU (uptr);                          /* pos not upd */
            status = MTSE_EOM;
            }
        else {
            uptr->pos = uptr->pos + sizeof (t_tpclnt);  /* spc over reclnt */
            if (tpcbc == TPC_TMK)                       /* tape mark? */
                status = MTSE_TMK;
            else
                uptr->pos = uptr->pos + ((tpcbc + 1) & ~1); /* spc over record */
            }
        break;


    case MTUF_F_P7B:
        for (sbc = 0, all_eof = 1; ; sbc++) {           /* loop thru record */
            sim_fread (&c, sizeof (uint8), 1, uptr->fileref);

            if (ferror (uptr->fileref)) {               /* error? */
                MT_SET_PNU (uptr);                      /* pos not upd */
                status = sim_tape_ioerr (uptr);
                break;
                }
            else if (feof (uptr->fileref)) {            /* eof? */
                if (sbc == 0)                           /* no data? eom */
                    status = MTSE_EOM;
                break;                                  /* treat like eor */
                }
            else if ((sbc != 0) && (c & P7B_SOR))       /* next record? */
                break;
            else if ((c & P7B_DPAR) != P7B_EOF)
                all_eof = 0;
            }

        if (status == MTSE_OK) {
            *bc = sbc;                                      /* save rec lnt */
            sim_fseek (uptr->fileref, uptr->pos, SEEK_SET); /* for read */
            uptr->pos = uptr->pos + sbc;                    /* spc over record */
            if (all_eof)                                    /* tape mark? */
                status = MTSE_TMK;
            }
        break;


    default:
        status = MTSE_FMT;
    }

return status;
}


/* Read record length reverse (internal routine).

   Inputs:
        uptr    =       pointer to tape unit
        bc      =       pointer to returned record length

   Outputs:
        status  =       operation status

   exit condition       tape position
   ------------------   -------------------------------------------
   unit unattached      unchanged
   read error           unchanged, PNU set if initial read
   beginning of tape    unchanged
   tape mark            updated
   other marker         updated
   tape runaway         updated
   data record          updated, sim_fread will read record forward

   This routine is called to set up a record read or spacing in the reverse
   direction.  On return, status is MTSE_OK if a data record, private marker, or
   reserved marker was read, or an MTSE error code if a standard marker (e.g.
   tape mark) was read, or an error occurred.  The file is positioned at the
   first byte of a data record, before a tape mark or private marker, or
   otherwise as indicated above.  In all cases, the successfully read marker or
   data record length word is returned via the "bc" pointer.

   When the extended SIMH format is enabled, then the variable addressed by the
   "bc" parameter must be set on entry to a bitmap of the object classes to
   return.  Each of the classes is represented by its corresponding bit, i.e.,
   bit 0 represents class 0, bit 1 for class 1, etc.  The routine will return
   only objects from the selected classes.  Unselected class objects will be
   ignored by skipping over them until the first selected class object is seen.
   This allows a simulator to declare those classes it understands (e.g.,
   standard classes 0 and 8, plus private classes 2 and 7) and those classes it
   wishes to ignore.  Erase gap markers are always skipped, and standard markers
   are always returned, so specifying an empty bitmap will perform the
   equivalent of a "space file forward," returning only when a tape mark or
   EOM/EOF is encountered.

   When standard SIMH format is enabled, standard classes 0 and 8 are
   automatically selected, and the entry value addressed by "bc" is ignored.

   The permissibility of data record lengths that are not multiples of the
   metadatum size presents a difficulty when reading through gaps.  If such an
   "odd length" record is written over a gap, half of a gap marker will exist
   immediately after the trailing record length.

   Reading in reverse presents a more complex problem than reading forward
   through gaps, because half of the marker is from the preceding trailing
   record length marker and therefore could be any of a range of values.
   However, that range is restricted by the SIMH tape specification to permit
   unambiguous detection of the condition.  The Class F assignments are:

     F0000000 - FFFDFFFF  Reserved for future use (available)
     FFFE0000 - FFFFFFFD  Reserved for erase gap interpretation
     FFFFFFFE             Erase gap (primary value)
     FFFFFFFF             End of medium

   Values within the reserved erase-gap interpretation subrange are as follows:

     FFFE0000 - FFFEFFFE  Illegal (would be seen as full gap in reverse reads)
     FFFEFFFF             Interpret as half-gap in forward reads
     FFFF0000 - FFFFFFFD  Interpret as half-gap in reverse reads

   A conforming writer will never write the illegal marker values, so that a
   conforming reader will be able to recognize the half-gap marker values.


   Implementation notes:

    1. The "sim_fread" call cannot return 0 in the absence of an error
       condition.  The preceding "sim_tape_bot" test ensures that "pos" >= 4, so
       "sim_fseek" will back up at least that far, so "sim_fread" will read at
       least one element.  If the call returns zero, an error must have
       occurred, so the "ferror" call must succeed.

    2. The "runaway_counter" cannot decrement to zero (or below) in the presence
       of an error that terminates the gap-search loop.  Therefore, the test
       after the loop exit need not check for error status.

    3. See the notes at "sim_tape_rdlntf" regarding the implementation of tape
       runaway detection.
*/

static t_stat sim_tape_rdlntr (UNIT *uptr, t_mtrlnt *bc)
{
const uint32 f = MT_GET_FMT (uptr);                     /* the tape format */
uint8        c;
t_bool       all_eof;
t_addr       ppos;
t_mtrlnt     sbc;
t_tpclnt     tpcbc;
t_mtrlnt     buffer [256];                              /* local tape buffer */
uint32       bufcntr, bufcap;                           /* buffer counter and capacity */
uint32       classbit;                                  /* bit representing the object class */
int32        runaway_counter, max_gap, sizeof_gap;      /* bytes remaining before runaway and bytes per gap */
t_addr       next_pos;                                  /* next record position */
t_stat       status = MTSE_OK;                          /* preset status return */
uint32       accept = MTB_STANDARD;                     /* preset the standard class acceptance set */

MT_CLR_PNU (uptr);                                      /* clear the position-not-updated flag */

if ((uptr->flags & UNIT_ATT) == 0)                      /* if the unit is not attached */
    return MTSE_UNATT;                                  /*   then quit with an error */

else if (sim_tape_bot (uptr))                           /* otherwise if the unit is positioned at the BOT */
    status = MTSE_BOT;                                  /*   then reading backward is not possible */

else switch (f) {                                       /* otherwise the read method depends on the tape format */

    case MTUF_F_EXT:
        accept = (uint32) (*bc);                        /* get the set of acceptable classes */

    /* fall through into the standard SIMH and E11 handler */

    case MTUF_F_STD:
    case MTUF_F_E11:
        max_gap = 25 * 12                               /* set the largest legal gap size in bytes */
                    * bpi [MT_DENS (uptr->dynflags)];   /*   corresponding to 25 feet of tape */

        if (max_gap == 0) {                             /* if tape density has not been not set */
            sizeof_gap = 0;                             /*   then disable runaway detection */
            max_gap = INT_MAX;                          /*     and allow gaps of any size */
            }

        else                                            /* otherwise */
            sizeof_gap = sizeof (t_mtrlnt);             /*   set the size of the gap */

        runaway_counter = max_gap;                      /* initialize the runaway counter */

        bufcntr = 0;                                    /* force an initial read */
        bufcap = 0;                                     /*   but of just one metadata marker */

        ppos = uptr->pos;                               /* save the initial tape position */

        do {                                            /* loop until an object is accepted or an error occurs */
            if (bufcntr == 0) {                         /*   then if the buffer is empty */
                if (sim_tape_bot (uptr)) {              /*     then if the search has backed into the BOT */
                    status = MTSE_BOT;                  /*       then quit with an error */
                    break;
                    }

                else if (bufcap == 0)                   /* otherwise if this is the initial read */
                    bufcap = 1;                         /*   then start with just one marker */

                else if (uptr->pos < sizeof (buffer))   /* otherwise if less than a full buffer remains */
                    bufcap = (uint32) uptr->pos         /*   then reduce the capacity accordingly */
                               / sizeof (t_mtrlnt);

                else                                    /* otherwise reset the capacity */
                    bufcap = sizeof (buffer)            /*   to the full size of the buffer */
                               / sizeof (buffer [0]);

                sim_fseek (uptr->fileref,                           /* seek back to the location */
                           uptr->pos - bufcap * sizeof (t_mtrlnt),  /*   corresponding to the start */
                           SEEK_SET);                               /*     of the buffer */

                bufcntr = sim_fread (buffer, sizeof (t_mtrlnt), /* fill the buffer */
                                     bufcap, uptr->fileref);    /*   with tape metadata */

                if (ferror (uptr->fileref)) {           /* if a file I/O error occurred */
                    if (uptr->pos == ppos)              /*   then if this is the initial read */
                        MT_SET_PNU (uptr);              /*     then set position not updated */

                    status = sim_tape_ioerr (uptr);     /* report the error and quit */
                    break;
                    }
                }

            *bc = buffer [--bufcntr];                   /* store the metadata marker value */

            uptr->pos = uptr->pos - sizeof (t_mtrlnt);  /* backspace over the marker */

            if (*bc == MTR_TMK) {                       /* if the marker is a tape mark */
                status = MTSE_TMK;                      /*   then quit with tape mark status */
                break;
                }

            else if (*bc == MTR_GAP)                    /* otherwise if the marker is a full gap */
                runaway_counter -= sizeof_gap;          /*   then decrement the gap counter */

            else if ((*bc & MTR_RHGAP) == MTR_RHGAP) {          /* otherwise if the marker is a half gap */
                uptr->pos = uptr->pos + sizeof (t_mtrlnt) / 2;  /*   then position forward to resync */

                bufcntr = 0;                            /* mark the buffer as invalid to force a read */
                runaway_counter -= sizeof_gap / 2;      /*   and decrement the gap counter by half */
                }

            else {                                      /* otherwise it is not a known marker */
                classbit = MTR_FB (*bc);                /*   so get the bit corresponding to the class */

                next_pos = uptr->pos - MTR_RL (*bc)     /* it it's a data record */
                             - sizeof (t_mtrlnt);       /*   then preset to the start of the record */

                if (f != MTUF_F_E11)                    /* if the format is not E11 */
                    next_pos -= MTR_RL (*bc) & 1;       /*   then record sizes are an even number of bytes */

                if (classbit & accept) {                /* if the class is accepted */
                    if (classbit == MTB_SMARK)          /*   then if it's a SIMH-reserved marker */
                        status = MTSE_RESERVED;         /*     then return reserved status */

                    else if (classbit == MTB_PMARK)     /*   otherwise if it's a private marker */
                        status = MTSE_OK;               /*     then return successful status */

                    else if (sim_fseek (uptr->fileref,                  /*   otherwise position to the start */
                                        next_pos + sizeof (t_mtrlnt),   /*     of the data area */
                                        SEEK_SET) == 0)                 /*       and if the seek succeeds */
                        uptr->pos = next_pos;                           /*         then position past the record */

                    else                                /*   otherwise the seek failed */
                        status = sim_tape_ioerr (uptr); /*     so quit with I/O error status */

                    break;                              /* acceptance terminates the search */
                    }

                else if (classbit & MTB_RECORDSET) {    /* otherwise if ignoring a data record */
                    uptr->pos = next_pos;               /*   then position before the record */

                    if (sim_fseek (uptr->fileref, uptr->pos, SEEK_SET)) {   /* set the new position; if it fails */
                        status = sim_tape_ioerr (uptr);                     /*   then quit with I/O error status */
                        break;
                        }

                    bufcntr = 0;                        /* mark the buffer as invalid to force a read */
                    }

                runaway_counter = max_gap;              /* ignoring a marker or record resets the counter */
                }
            }
        while (runaway_counter > 0);                    /* continue searching until runaway occurs */

        if (runaway_counter <= 0)                       /* if a tape runaway occurred */
            status = MTSE_RUNAWAY;                      /*   then report it */

        break;                                          /* end of case */


    case MTUF_F_TPC:
        ppos = sim_tape_tpc_fnd (uptr, (t_addr *) uptr->filebuf); /* find prev rec */
        sim_fseek (uptr->fileref, ppos, SEEK_SET);      /* position */
        sim_fread (&tpcbc, sizeof (t_tpclnt), 1, uptr->fileref);
        *bc = tpcbc;                                    /* save rec lnt */

        if (ferror (uptr->fileref))                     /* error? */
            status = sim_tape_ioerr (uptr);
        else if (feof (uptr->fileref))                  /* eof? */
            status = MTSE_EOM;
        else {
            uptr->pos = ppos;                           /* spc over record */
            if (*bc == MTR_TMK)                         /* tape mark? */
                status = MTSE_TMK;
            else
                sim_fseek (uptr->fileref, uptr->pos + sizeof (t_tpclnt), SEEK_SET);
            }
        break;


    case MTUF_F_P7B:
        for (sbc = 1, all_eof = 1; (t_addr) sbc <= uptr->pos ; sbc++) {
            sim_fseek (uptr->fileref, uptr->pos - sbc, SEEK_SET);
            sim_fread (&c, sizeof (uint8), 1, uptr->fileref);

            if (ferror (uptr->fileref)) {               /* error? */
                status = sim_tape_ioerr (uptr);
                break;
                }
            else if (feof (uptr->fileref)) {            /* eof? */
                status = MTSE_EOM;
                break;
                }
            else {
                if ((c & P7B_DPAR) != P7B_EOF)
                    all_eof = 0;
                if (c & P7B_SOR)                        /* start of record? */
                    break;
                }
            }

        if (status == MTSE_OK) {
            uptr->pos = uptr->pos - sbc;                    /* update position */
            *bc = sbc;                                      /* save rec lnt */
            sim_fseek (uptr->fileref, uptr->pos, SEEK_SET); /* for read */
            if (all_eof)                                    /* tape mark? */
                status = MTSE_TMK;
            }
        break;


    default:
        status = MTSE_FMT;
    }

return status;
}


/* Read a data record or tape marker.

   Read the data record or tape marker a the current tape position in the
   indicated direction and return it via the supplied buffer or marker pointers,
   respectively.

   On entry, the "uptr" parameter points at the UNIT structure describing the
   tape device, "buffer" points at a buffer large enough to receive a retrieved
   data record, "class_count" points at a variable that receives the data record
   length word containing the record class and length or the tape marker value,
   "bufsize" indicates the size of the buffer in bytes, and "reverse" is TRUE if
   the record is to be read in the reverse direction and FALSE if the record is
   to be read in the forward direction.

   If the tape format is extended SIMH, then "class_count" must point at a value
   containing a bitmap of the desired record and marker classes to read.  Each
   class is represented by its corresponding bit.  Classes present in the tape
   image but not in the bitmap are skipped until an object of the specified
   class is read.  The standard markers (tape mark, etc.) are always read and
   interpreted.

   A successful read of a data record returns the data in the buffer and the
   record class and length via the "class_count" parameter.  A successful marker
   read returns the marker value via the "class_count" parameter; the buffer is
   not used.

   For all other tape formats, the entry value indicated by "class_count" is
   ignored, and all items supported by the specified format are returned.
   Successful reads return data in the buffer and the record length via the
   "class_count" parameter.

   The result of the read is returned as the value of the function, as follows:

     Status          Condition
     -------------   ---------------------------------------------
     MTSE_OK         Successful read of a good data record
     MTSE_RECE       Successful read of a bad data record
     MTSE_TMK        Successful read of a tape mark
     MTSE_RESERVED   Successful read of a reserved marker

     MTSE_UNATT      The tape unit is not attached
     MTSE_IOERR      A host I/O error occurred
     MTSE_FMT        An invalid tape format is selected
     MTSE_BOT        Reading stopped at the beginning of the tape
     MTSE_EOM        Reading stopped at the end of the tape
     MTSE_RUNAWAY    Reading did not encounter any data
     MTSE_INVRL      The record is larger than the supplied buffer
                     or the record is incomplete
*/

static t_stat tape_read (UNIT *uptr, uint8 *buffer, t_mtrlnt *class_count, t_mtrlnt bufsize, t_bool reverse)
{
const uint32 f = MT_GET_FMT (uptr);                     /* the tape format */
t_mtrlnt     cbc, rbc;
t_addr       opos;
t_stat       st;

cbc = *class_count;                                     /* get the acceptance mask */
opos = uptr->pos;                                       /*   and save the original file position */

if (reverse)                                            /* for a reverse read */
    st = sim_tape_rdlntr (uptr, &cbc);                  /*   get the preceding record length */
else                                                    /* otherwise */
    st = sim_tape_rdlntf (uptr, &cbc);                  /*   get the following record length */

if (st != MTSE_OK                                       /* if the read failed */
  || (MTR_FB (cbc) & MTB_MARKERSET)) {                  /*   or it returned a marker */
    if (f == MTUF_F_EXT)                                /*     then if the format is extended SIMH */
        *class_count = cbc;                             /*       then return the marker value */

    return st;                                          /* return the status */
    }

rbc = MTR_RL (cbc);                                     /* get the record length */

if (f == MTUF_F_EXT)                                    /* if the format is extended SIMH */
    *class_count = cbc;                                 /*   then return the class and length */
else                                                    /* otherwise */
    *class_count = rbc;                                 /*   return just the length */

if (rbc > bufsize)                                      /* if the record won't fit in the buffer */
    st = MTSE_INVRL;                                    /*   then return invalid length status */

else {                                                      /* otherwise */
    sim_fread (buffer, sizeof (uint8), rbc, uptr->fileref); /*   read the data payload into the supplied buffer */

    if (ferror (uptr->fileref))                         /* if a host I/O error occurred */
        st = sim_tape_ioerr (uptr);                     /*    then return I/O error status */

    else if (feof (uptr->fileref))                      /* otherwise if the read was incomplete */
        st = MTSE_INVRL;                                /*   then report a record length error */

    else if (f == MTUF_F_P7B)                            /* otherwise if the format is P7B */
        buffer [0] = buffer [0] & P7B_DPAR;              /*   then strip the start-of-record flag */
    }

if (st != MTSE_OK) {                                    /* if the read failed */
    MT_SET_PNU (uptr);                                  /*   then set the position not updated flag */
    uptr->pos = opos;                                   /*     and restore the original position */
    return st;                                          /*       and return the failure status */
    }

else if (MTR_CF (cbc) == MTC_BAD)                       /* otherwise if a bad record was read */
    return MTSE_RECE;                                   /*   then report a data error */

else                                                    /* otherwise */
    return MTSE_OK;                                     /*   report a successful read */
}


/* Read record or marker forward.

   Inputs:
        uptr    =       pointer to tape unit
        buf     =       pointer to buffer
        bc      =       pointer to returned class/record length
        max     =       maximum record size

   Outputs:
        status  =       operation status
*/

t_stat sim_tape_rdrecf (UNIT *uptr, uint8 *buf, t_mtrlnt *bc, t_mtrlnt max)
{
return tape_read (uptr, buf, bc, max, FALSE);           /* read and return the next record or marker */
}


/* Read record or marker reverse.

   Inputs:
        uptr    =       pointer to tape unit
        buf     =       pointer to buffer
        bc      =       pointer to returned class/record length
        max     =       maximum record size

   Outputs:
        status  =       operation status
*/

t_stat sim_tape_rdrecr (UNIT *uptr, uint8 *buf, t_mtrlnt *bc, t_mtrlnt max)
{
return tape_read (uptr, buf, bc, max, TRUE);           /* read and return the prior record or marker */
}


/* Write a data record forward.

   Write a data record at the current tape position and return the status of the
   operation.

   On entry, the "uptr" parameter points at the UNIT structure describing the
   tape device, "buf" points at the buffer containing the data, and "clbc"
   contains the class and record length.

   If the tape format is extended SIMH, then "clbc" must contain a standard or
   private data record class and if the class is 0 (i.e., a good data record),
   then the record length must be non-zero.  For all other tape formats, the
   class must be 0 or 8 (good or bad data record); a record length of zero is
   treated as a NOP for these formats.

   The result of the write is returned as the value of the function, as follows:

     Status          Condition
     -------------   -------------------------------------------------
     MTSE_OK         Successful write of the data record

     MTSE_UNATT      The tape unit is not attached
     MTSE_WRP        The tape unit is write protected
     MTSE_IOERR      A host I/O error occurred
     MTSE_INVRL      The record length is improper or too long
     MTSE_RESERVED   The record class is reserved or is a marker class
     MTSE_FMT        The tape format does not support the record class
*/

t_stat sim_tape_wrrecf (UNIT *uptr, uint8 *buf, t_mtrlnt clbc)
{
const uint32 f = MT_GET_FMT (uptr);                     /* the tape format */
t_mtrlnt     sbc;
uint32       classbit;

MT_CLR_PNU (uptr);                                      /* clear the position-not-updated flag */

sbc      = MTR_RL (clbc);                               /* get the record length */
classbit = MTR_FB (clbc);                               /*   and the class field bit */

if (f == MTUF_F_EXT) {                                  /* if the format is extended SIMH */
    if (! (classbit & MTB_EXTENDED))                    /*   then if not in the extended record class */
        return MTSE_RESERVED;                           /*     then report a reserved class error */
    else if (sbc == 0 && classbit == MTB_GOOD)          /*   otherwise if the length of a good record is zero */
        return MTSE_INVRL;                              /*     then report an invalid length error */
    }

else if (! (classbit & MTB_STANDARD))                   /* otherwise if the class is not a standard record */
    return MTSE_FMT;                                    /*   then report a format error */

else if (sbc == 0 && classbit == MTB_GOOD)              /* otherwise if the length of a good record is zero */
    return MTSE_OK;                                     /*   then treat it as a NOP */

else if (sbc > MTR_MAXLEN)                              /* otherwise if the record is too long */
    return MTSE_INVRL;                                  /*   then report an invalid length error */

if ((uptr->flags & UNIT_ATT) == 0)                      /* if the unit is not attached */
    return MTSE_UNATT;                                  /*   then report it */

else if (sim_tape_wrp (uptr))                           /* otherwise if the tape is write protected */
    return MTSE_WRP;                                    /*   then report it */

sim_fseek (uptr->fileref, uptr->pos, SEEK_SET);         /* set the tape position */

switch (f) {                                            /* dispatch on the format */

    case MTUF_F_STD:                                    /* standard */
    case MTUF_F_EXT:                                    /* extended standard */
        sbc = (sbc + 1) & ~1;                           /* pad odd length */

    /* fall through into the E11 handler */

    case MTUF_F_E11:                                    /* E11 */
        sim_fwrite (&clbc, sizeof (t_mtrlnt), 1, uptr->fileref);
        sim_fwrite (buf, sizeof (uint8), sbc, uptr->fileref);
        sim_fwrite (&clbc, sizeof (t_mtrlnt), 1, uptr->fileref);
        if (ferror (uptr->fileref)) {                   /* error? */
            MT_SET_PNU (uptr);
            return sim_tape_ioerr (uptr);
            }
        uptr->pos = uptr->pos + sbc + (2 * sizeof (t_mtrlnt));  /* move tape */
        break;

    case MTUF_F_P7B:                                    /* Pierce 7B */
        buf[0] = buf[0] | P7B_SOR;                      /* mark start of rec */
        sim_fwrite (buf, sizeof (uint8), sbc, uptr->fileref);
        sim_fwrite (buf, sizeof (uint8), 1, uptr->fileref); /* delimit rec */
        if (ferror (uptr->fileref)) {                   /* error? */
            MT_SET_PNU (uptr);
            return sim_tape_ioerr (uptr);
            }
        uptr->pos = uptr->pos + sbc;                    /* move tape */
        break;
        }

return MTSE_OK;
}


/* Write metadata forward (internal routine) */

static t_stat sim_tape_wrdata (UNIT *uptr, uint32 dat)
{
MT_CLR_PNU (uptr);
if ((uptr->flags & UNIT_ATT) == 0)                      /* not attached? */
    return MTSE_UNATT;
if (sim_tape_wrp (uptr))                                /* write prot? */
    return MTSE_WRP;
sim_fseek (uptr->fileref, uptr->pos, SEEK_SET);         /* set pos */
sim_fwrite (&dat, sizeof (t_mtrlnt), 1, uptr->fileref);
if (ferror (uptr->fileref)) {                           /* error? */
    MT_SET_PNU (uptr);
    return sim_tape_ioerr (uptr);
    }
uptr->pos = uptr->pos + sizeof (t_mtrlnt);              /* move tape */
return MTSE_OK;
}


/* Write a private marker.

   Write a private marker value at the current tape position and return the
   status of the operation.

   On entry, the "uptr" parameter points at the UNIT structure describing the
   tape device, and "mk" contains the marker class and value.  The tape format
   must be extended SIMH, and "mk" must be a member of the private marker class.

   The result of the write is returned as the value of the function, as follows:

     Status          Condition
     -------------   ------------------------------------------------
     MTSE_OK         Successful write of the marker

     MTSE_UNATT      The tape unit is not attached
     MTSE_WRP        The tape unit is write protected
     MTSE_IOERR      A host I/O error occurred
     MTSE_RESERVED   The class is not the private marker class
     MTSE_FMT        The tape format does not support private markers
*/

t_stat sim_tape_wrmrk (UNIT *uptr, t_mtrlnt mk)
{
if (MT_GET_FMT (uptr) != MTUF_F_EXT)                    /* if the format is not extended SIMH */
    return MTSE_FMT;                                    /*   then report a format error */

else if (MTR_CF (mk) != MTC_PMARK)                      /* otherwise if the marker is not private */
    return MTSE_RESERVED;                               /*   then report a reserved class error */

else                                                    /* otherwise */
    return sim_tape_wrdata (uptr, mk);                  /*   write the marker to the tape */
}


/* Write tape a mark */

t_stat sim_tape_wrtmk (UNIT *uptr)
{
if (MT_GET_FMT (uptr) == MTUF_F_P7B) {                  /* P7B? */
    uint8 buf = P7B_EOF;                                /* eof mark */
    return sim_tape_wrrecf (uptr, &buf, 1);             /* write char */
    }
return sim_tape_wrdata (uptr, MTR_TMK);
}


/* Write an end of medium */

t_stat sim_tape_wreom (UNIT *uptr)
{
t_stat result;

if (MT_GET_FMT (uptr) == MTUF_F_P7B)                    /* cant do P7B */
    return MTSE_FMT;

result = sim_tape_wrdata (uptr, MTR_EOM);               /* write the EOM marker */

uptr->pos = uptr->pos - sizeof (t_mtrlnt);              /* restore original tape position */
MT_SET_PNU (uptr);                                      /* indicate that position was not updated */

return result;
}


/* Erase a gap of the specified number of bytes (internal routine).

   This routine will write a gap of the requested number of bytes at the current
   position of the file attached to the supplied unit.

   On entry, the file is positioned to the start of the gap as indicated by the
   "uptr->pos" value.  The minimum gap size allowed is four bytes (one erase gap
   marker); smaller values will be rounded up.  As the SIMH tape format allows
   erasures only in multiples of two bytes, an odd byte count is rounded up to
   the next even value.

   If the requested size will not accommodate an integral number of gap markers,
   a leading half-gap marker is written first.  Then the required number of gap
   markers are written to fill the specified gap.  To improve efficiency, each
   "sim_fwrite" call writes multiple gaps.

   If a host file I/O error occurs while writing, the file position is restored,
   the position-not-updated flag is set, the error is reported to the console,
   and the routine returns MTSE_IOERR.  Otherwise, the routine returns MTSE_OK.


   Implementation notes:

    1. Erase gaps are currently supported only in standard and extended SIMH
       tape formats.

    2. There is no easy way to initialize the "gaps" array statically, so we do
       it at run-time.  However, being a static array, the initialization is
       only performed once and the elements are guaranteed to be zero when this
       routine is called for the first time

    3. The "half_gap" array wants to be constant, but "sim_fwrite" does not
       declare a compatible buffer pointer parameter.

    4. A half-gap cannot be written by itself, as interpretation when reading
       would be indeterminate.
*/

static t_stat tape_erase (UNIT *uptr, t_mtrlnt byte_count)
{
static t_mtrlnt gaps [256];                             /* a block of erase gaps */
static uint8    half_gap [2] = { 0xFF, 0xFF };          /* upper half of an erase gap */
const  uint32   buffer_size  = sizeof gaps / sizeof gaps [0];
const  uint32   meta_size    = sizeof (t_mtrlnt);       /* the number of bytes per metadatum */
const  t_addr   gap_pos      = uptr->pos;               /* the file position where the gap will start */
uint32          count, marker_count;

if (gaps [0] == 0)                                      /* if the gap block has not been initialized */
    for (count = 0; count < buffer_size; count++)       /*   then fill the block with erase gaps */
        gaps [count] = MTR_GAP;                         /*     to improve write performance */

sim_fseek (uptr->fileref, uptr->pos, SEEK_SET);         /* seek to the start of the gap */

byte_count = (byte_count + 1) & ~1;                     /* round the count to an even number */

if (byte_count < meta_size)                             /* if the size is smaller than an erase gap marker */
    byte_count = meta_size;                             /*   then increase the size to one marker */

else if (byte_count % meta_size > 0) {                  /* otherwise if an integral number of markers won't fit */
    sim_fwrite (half_gap, sizeof (uint8), 2,            /*   then start the gap */
                uptr->fileref);                         /*     with a half-gap marker */

    uptr->pos  = uptr->pos  + sizeof half_gap;          /* advance the tape position */
    byte_count = byte_count - sizeof half_gap;          /*   and drop the byte count for the half-gap */
    }

marker_count = byte_count / meta_size;                  /* get the count of full gap markers */

while (marker_count > 0) {                              /* while full gaps are needed */
    if (marker_count > buffer_size)                     /* if more than a full block is needed */
        count = buffer_size;                            /*   then write a full block of gaps */
    else                                                /* otherwise */
        count = marker_count;                           /*   write the remaining size needed */

    sim_fwrite (gaps, meta_size, count, uptr->fileref); /* write the erase gap */

    marker_count = marker_count - count;                /* reduce the count by the amount erased */
    }

if (ferror (uptr->fileref)) {                           /* if a host I/O error occurred */
    uptr->pos = gap_pos;                                /*   then reposition back to the gap start */

    MT_SET_PNU (uptr);                                  /* report that the position was not updated */
    return sim_tape_ioerr (uptr);                       /*   and that an error occurred */
    }

else {                                                  /* otherwise the writes were successful */
    uptr->pos = uptr->pos + byte_count;                 /*   so advance the tape position past the gap */

    MT_CLR_PNU (uptr);                                  /* report that the position was updated */
    return MTSE_OK;                                     /*   and return success */
    }
}


/* Erase a gap in the forward direction (internal routine).

   An erase gap is written in the forward direction on the tape unit specified
   by "uptr" for the number of bytes specified by "gap_size".  The status of the
   operation is returned, and the file position is altered as follows:

     Exit Condition       File Position
     ------------------   ------------------
     unit unattached      unchanged
     unsupported format   unchanged
     write protected      unchanged
     read error           unchanged, PNU set
     write error          unchanged, PNU set
     gap written          updated

   If the requested byte count equals the metadatum size, then the routine
   succeeds only if it can overlay a single metadatum (i.e., a tape mark, an
   end-of-medium marker, or an existing erase gap marker); otherwise, the file
   position is not altered, PNU is set, and MTSE_INVRL (invalid record length)
   status is returned.

   An erase gap is represented in the tape image file by a special metadata
   value repeated throughout the gap.  The value is chosen so that it is still
   recognizable even if it has been "cut in half" by a subsequent data overwrite
   that does not end on a metadatum-sized boundary.  In addition, a range of
   metadata values are reserved for detection in the reverse direction.

   This implementation supports erasing gaps in the middle of a populated tape
   image and will always produce a valid image.  It also produces valid images
   when overwriting gaps with data records, with one exception: a data write
   that leaves only two bytes of gap remaining will produce an invalid tape.
   This limitation is deemed acceptable, as it is analogous to the existing
   limitation that data records cannot overwrite other data records without
   producing an invalid tape.

   To write an erase gap, the implementation uses one of two approaches,
   depending on whether or not the current tape position is at EOM.  Erasing at
   EOM presents no special difficulties; gap metadata markers are written for
   the prescribed number of bytes.  If the tape is not at EOM, then erasing must
   take into account the existing record structure to ensure that a valid tape
   image is maintained.

   The general approach is to erase for the nominal number of bytes but to
   increase that length if necessary to ensure that a partially overwritten data
   record at the end of the gap can be altered to maintain validity.  Because
   the smallest legal tape record requires space for two metadata markers plus
   two data bytes, an erasure that would leave less than that is increased to
   consume the entire record.  Otherwise, the final record is truncated by
   rewriting the leading and trailing length words appropriately.

   When reading in either direction, gap metadata markers are ignored (skipped)
   until a record length header, non-gap marker, or physical EOF is encountered.
   Thus, tape images containing gap metadata are transparent to the calling
   simulator (unless tape runaway support is enabled -- see the notes at
   "sim_tape_rdlntf" for details).

   If the current tape format supports erase gaps, then this routine will write
   a gap of the requested size.  If the format does not, then no action will be
   taken, and MTSE_OK status will be returned.  This allows a device simulator
   that supports writing erase gaps to use the same code without worrying about
   the tape format currently selected by the user.  A request for an erase gap
   of zero length also succeeds with no action taken.

   Considerations when reading erase gaps are discussed in more detail in the
   comments of the "sim_tape_rdlntf" routine.


   The scan of an existing tape image before erasing proceeds as follows.

   After preliminary access checks (i.e., image is attached and is writable),
   the file is positioned to the start of the area to be erased.  The routine
   then enters a loop that reads data items and accumulates the areas to be
   erased until the required number of bytes have been examined.  When that
   happens, the gap is written at the original position, extended as necessary
   to maintain tape integrity.  If an error occurs, the scan is aborted, and the
   appropriate error code is returned to the caller.

   Each pass of the loop begins by reading the next metadatum in the file.  If a
   host I/O error occurs, the scan is aborted with an appropriate error return.
   If an EOM metadatum was read, or the physical EOF was encountered, the scan
   is completed by allocating the remaining gap space unconditionally.

   If a gap or tape mark metadatum was read, the position is advanced over the
   marker, and the scan continues.  If a half-gap was read, the position is
   adjusted to align with the next full metadatum, and the scan continues.  If
   the metadatum is not one of these items, it must be a data record leading
   length marker.  If the caller wants only a single metadatum erased, the
   routine returns an invalid record length error.

   Before adding part or all of the data record to the accumulated erase area,
   record integrity is checked.  Verification of the trailing length marker is
   done in two steps.  If the marker would be positioned beyond the end of the
   file, then the tape image is considered to be invalid, and the scan is
   terminated.  If the location is within the file, the position is temporarily
   moved to the location of the trailing length marker, which is read and
   compared to the leading length marker.  If the read fails, or the markers do
   not compare, then the image is invalid, and the scan is completed by
   allocating the remaining gap space unconditionally.

   If the markers compare, then a check is made to see if the data record is
   contained wholly within the area to be erased or if it extends beyond the end
   of the erasure.  In the first case, the space occupied by the record is
   simply added to the accumulated area.  In the second case, however, the
   record must be truncated to maintain image validity.

   Truncation is possible only if a valid record can be written into the
   space occupied by the remaining part of the record.  The smallest legal
   record is two data bytes long, and such a record occupies ten bytes,
   including the pair of four-byte length markers.  If the remaining space is
   too small, the gap is extended to consume the full record to avoid leaving an
   invalid area.

   If the size of the remaining record after the erasure is large enough, the
   trailing length word is rewritten for the new, shorter size, and then the
   leading length word immediately following the erased area is written to
   match.

   Once gap allocation is complete, the loop terminates, and the file is
   repositioned to the start of the gap area.  If the loop terminated for an
   error, it is returned with PNU set.  Otherwise, the new gap, lengthened if
   necessary, is written, and PNU is cleared.


   Implementation notes:

    1. Erase gaps are currently supported only in standard and extended SIMH
       tape formats.

    2. Metadatum reads either succeed and return 1 or fail and return 0.  If a
       read fails, and "ferror" returns false, then it must have read into the
       end of the file (only these three outcomes are possible).

    3. The area scan is necessary for tape image integrity to ensure that a data
       record straddling the end of the erasure is truncated appropriately.  The
       scan is guaranteed to succeed only if it begins at a valid metadatum.  If
       it begins in the middle of a previously overwritten data record, then the
       scan will interpret old data values as tape formatting markers.  The data
       record sanity checks attempt to recover from this situation, but it is
       still possible to corrupt valid data that follows an erasure of an
       invalid area (e.g., if the leading and trailing length words happen
       to match but actually represent previously recorded data rather than
       record metadata).  If an application knows that the erased area will
       not contain valid formatting, the "sim_tape_erase" routine should be used
       instead, as it erases without first scanning the area.

    4. Truncating an existing data record corresponds to overwriting part of a
       record on a real tape.  Reading such a record on a real drive would
       produce CRC errors, due to the lost portion.  In simulation, we could
       change a good record (Class 0) to a bad record (Class 8).  However, this
       is not possible for private or reserved record classes, as that would
       change the classification (consider that a private class that had
       been ignored would not be once it had been truncated and changed to Class
       8).  Given that there is no good general solution, we do not modify
       classes for truncated records, as reading a partially erased record is an
       "all bets are off" operation.
*/

static t_stat tape_erase_fwd (UNIT *uptr, t_mtrlnt gap_size)
{
const t_addr gap_pos = uptr->pos;                       /* the file position where the gap will start */
const uint32 format = MT_GET_FMT (uptr);                /* the tape format */
const uint32 meta_size = sizeof (t_mtrlnt);             /* the number of bytes per metadatum */
const uint32 min_rec_size = 2 + sizeof (t_mtrlnt) * 2;  /* the smallest data record size */
size_t       xfer;
t_mtrlnt     meta, sbc, new_len, rec_size;
uint32       file_size;
int32        gap_needed = (int32) gap_size;             /* the gap remaining to be allocated from the tape */
uint32       gap_alloc = 0;                             /* the gap currently allocated from the tape */
t_stat       status = MTSE_OK;                          /* the status of the last operation */

MT_CLR_PNU (uptr);                                      /* clear the position-not-updated flag */

if ((uptr->flags & UNIT_ATT) == 0)                      /* if the unit is not attached */
    return MTSE_UNATT;                                  /*   then we cannot proceed */

else if (sim_tape_wrp (uptr))                           /* otherwise if the unit is write protected */
    return MTSE_WRP;                                    /*   then we cannot write */

else if (gap_size == 0                                  /* otherwise if the gap is zero length */
 || (format != MTUF_F_STD && format != MTUF_F_EXT))     /*   or gaps are not supported */
    return MTSE_OK;                                     /*     then take no action */

MT_SET_PNU (uptr);                                      /* errors from here on do not update the position */

file_size = sim_fsize (uptr->fileref);                  /* get the file size */

if (sim_fseek (uptr->fileref, uptr->pos, SEEK_SET))     /* position the tape; if it fails */
    return sim_tape_ioerr (uptr);                       /*   then quit with I/O error status */


do {                                                        /* scan the area to be erased */
    xfer = sim_fread (&meta, meta_size, 1, uptr->fileref);  /*   starting with the next metadatum in the file */

    if (ferror (uptr->fileref)) {                       /* if a read error occurred */
        status = sim_tape_ioerr (uptr);                 /*   then report an I/O error */
        break;                                          /*     and quit the search */
        }

    else if (xfer == 1)                                 /* otherwise if we had a good read */
        uptr->pos = uptr->pos + meta_size;              /*   then move the tape over the datum */

    if ((xfer == 0) || (meta == MTR_EOM)) {             /* if the physical EOF or an EOM marker is seen */
        gap_alloc = gap_alloc + gap_needed;             /*   then allocate the remainder of the space */
        break;                                          /*     and terminate the search */
        }

    else if ((meta == MTR_GAP) || (meta == MTR_TMK)) {  /* otherwise if a gap or tape mark is seen */
        gap_alloc = gap_alloc + meta_size;              /*   then allocate the marker space */
        gap_needed = gap_needed - meta_size;            /*     and reduce the amount remaining */
        }

    else if (gap_size == meta_size) {                   /* otherwise if the request is for a single metadatum */
        status = MTSE_INVRL;                            /*   then report an invalid record length error */
        break;                                          /*     as we're not erasing a metadatum as required */
        }

    else if (meta == MTR_FHGAP) {                       /* otherwise if a half-gap is seen */
        uptr->pos = uptr->pos - meta_size / 2;          /*   then back up to resync */

        if (sim_fseek (uptr->fileref, uptr->pos, SEEK_SET)) {   /* position the tape; if it fails */
            status = sim_tape_ioerr (uptr);                     /*   then report an I/O error */
            break;                                              /*     and quit the search */
            }

        gap_alloc = gap_alloc + meta_size / 2;          /* allocate the marker space */
        gap_needed = gap_needed - meta_size / 2;        /*   and reduce the amount remaining */
        }

    else if (uptr->pos + MTR_RL (meta) + meta_size > file_size) {   /* otherwise if it cannot be a data record */
        gap_alloc = gap_alloc + gap_needed;                         /*   then presume an overwritten tape */
        break;                                                      /*     and allocate the remainder of the space */
        }

    else {                                              /* otherwise it may be a data record */
        sbc = MTR_RL (meta);                            /*   so get the data length */
        rec_size = ((sbc + 1) & ~1) + meta_size * 2;    /*     and the overall record size in bytes */

        uptr->pos = uptr->pos + (sbc + 1) & ~1;         /* position to the trailing length marker */

        if (sim_fseek (uptr->fileref, uptr->pos, SEEK_SET)) {   /* position the tape; if it fails */
            status = sim_tape_ioerr (uptr);                     /*   then report an I/O error */
            break;                                              /*     and quit the search */
            }

        xfer = sim_fread (&meta, meta_size, 1, uptr->fileref);  /* read the metadatum */

        if (ferror (uptr->fileref)) {                   /* if a read error occurred */
            status = sim_tape_ioerr (uptr);             /*   then report an I/O error */
            break;                                      /*     and quit the search */
            }

        else if ((xfer != 1) || (sbc != MTR_RL (meta))) {   /* otherwise if the marker is bad or does not compare */
            gap_alloc = gap_alloc + gap_needed;             /*   then presume an overwritten tape */
            break;                                          /*     and allocate the remainder of the space */
            }

        else if (rec_size < gap_needed + min_rec_size) {    /* otherwise if the record is smaller than needed */
            uptr->pos = uptr->pos + meta_size;              /*   then skip over the record */
            gap_alloc = gap_alloc + rec_size;               /*     and allocate the record space */
            gap_needed = gap_needed - rec_size;             /*       and reduce the amount remaining */
            }

        else {                                              /* otherwise the size is larger than needed */
            new_len = MTR_CF (meta) | (sbc - gap_needed);   /*   so get the shortened record length */

            status = sim_tape_wrdata (uptr, new_len);   /* rewrite the trailing length marker */

            uptr->pos = uptr->pos - 2 * meta_size       /* move the position */
                          - sbc + gap_needed;           /*   back to the leading length marker */

            if (status == MTSE_OK)                          /* if the trailing write succeeded */
                status = sim_tape_wrdata (uptr, new_len);   /*   then rewrite the leading length marker */

            gap_alloc = gap_alloc + gap_needed;         /* the record provides the rest of the gap */
            break;                                      /*   and no more space is needed */
            }
        }
    }
while (gap_needed > 0);                                 /* loop until all of the gap has been allocated */


uptr->pos = gap_pos;                                    /* reposition to the start of the gap */

if (status == MTSE_OK)                                  /* if the scan was successful */
    return tape_erase (uptr, gap_alloc);                /*   then return the status of the erasure */
else                                                    /* otherwise the search failed */
    return status;                                      /*   so return the status code with PNU */
}


/* Erase a gap in the reverse direction (internal routine).

   An erase gap is written in the reverse direction on the tape unit specified
   by "uptr" for the number of bytes specified by "gap_size".  The status of the
   operation is returned, and the file position is altered as follows:

     Exit Condition       File Position
     ------------------   ------------------
     unit unattached      unchanged
     unsupported format   unchanged
     write protected      unchanged
     read error           unchanged, PNU set
     write error          unchanged, PNU set
     gap written          updated

   If the requested byte count equals the metadatum size, then the routine
   succeeds only if it can overlay a single metadatum (i.e., a tape mark, an
   end-of-medium marker, or an existing erase gap marker); otherwise, the file
   position is not altered, PNU is set, and MTSE_INVRL (invalid record length)
   status is returned.


   Implementation notes:

    1. Erase gaps are currently supported only in standard and extended SIMH
       tape formats.

    2. Erasing a record in the reverse direction currently succeeds only if the
       gap requested occupies the same space as the record located immediately
       before the current file position.  This limitation may be lifted in a
       future update.

    3. The "sim_fread" call cannot return 0 in the absence of an error
       condition.  The preceding "sim_tape_bot" test ensures that "pos" >= 4, so
       "sim_fseek" will back up at least that far, so "sim_fread" will read at
       least one element.  If the call returns zero, an error must have
       occurred, so the "ferror" call must succeed.
*/

static t_stat tape_erase_rev (UNIT *uptr, t_mtrlnt gap_size)
{
const uint32 format = MT_GET_FMT (uptr);                /* the tape format */
const uint32 meta_size = sizeof (t_mtrlnt);             /* the number of bytes per metadatum */
t_stat       status;
t_mtrlnt     rec_size, metadatum;
t_addr       gap_pos;
size_t       xfer;

MT_CLR_PNU (uptr);                                      /* clear the position-not-updated flag */

if ((uptr->flags & UNIT_ATT) == 0)                      /* if the unit is not attached */
    return MTSE_UNATT;                                  /*   then we cannot proceed */

else if (sim_tape_wrp (uptr))                           /* otherwise if the unit is write protected */
    return MTSE_WRP;                                    /*   then we cannot write */

else if (gap_size == 0                                  /* otherwise if the gap is zero length */
 || (format != MTUF_F_STD && format != MTUF_F_EXT))     /*   or gaps are not supported */
    return MTSE_OK;                                     /*     then take no action */

gap_pos = uptr->pos;                                    /* save the starting position */

if (gap_size == meta_size) {                            /* if the request is for a single metadatum */
    if (sim_tape_bot (uptr))                            /*   then if the unit is positioned at the BOT */
        return MTSE_BOT;                                /*     then erasing backward is not possible */
    else                                                /*   otherwise */
        uptr->pos -= meta_size;                         /*     back up the file pointer */

    if (sim_fseek (uptr->fileref, uptr->pos, SEEK_SET)) /* position the tape; if it fails */
        return sim_tape_ioerr (uptr);                   /*   then quit with I/O error status */

    sim_fread (&metadatum, meta_size, 1, uptr->fileref);    /* read a metadatum */

    if (ferror (uptr->fileref))                             /* if a file I/O error occurred */
        return sim_tape_ioerr (uptr);                       /*   then report the error and quit */

    else if (metadatum == MTR_TMK)                          /* otherwise if a tape mark is present */
        if (sim_fseek (uptr->fileref, uptr->pos, SEEK_SET)) /*   then reposition the tape; if it fails */
            return sim_tape_ioerr (uptr);                   /*     then quit with I/O error status */

        else {                                              /*   otherwise */
            metadatum = MTR_GAP;                            /*     replace it with an erase gap marker */

            xfer = sim_fwrite (&metadatum, meta_size,   /* write the gap marker */
                               1, uptr->fileref);

            if (ferror (uptr->fileref) || xfer == 0)    /* if a file I/O error occurred */
                return sim_tape_ioerr (uptr);           /* report the error and quit */
            else                                        /* otherwise the write succeeded */
                status = MTSE_OK;                       /*   so return success */
            }

    else if (metadatum == MTR_GAP)                      /* otherwise if a gap already exists */
        status = MTSE_OK;                               /*   then take no additional action */

    else {                                              /* otherwise a data record is present */
        uptr->pos = gap_pos;                            /*   so restore the starting position */
        return MTSE_INVRL;                              /*     and fail with invalid record length status */
        }
    }

else {                                                  /* otherwise it's an erase record request */
    status = sim_tape_rdlntr (uptr, &rec_size);         /*   so get the length of the preceding record */

    if (status == MTSE_OK                               /* if the read succeeded */
      && gap_size == rec_size + 2 * meta_size) {        /*   and the gap will exactly overlay the record */
        gap_pos = uptr->pos;                            /*     then save the gap start position */

        status = tape_erase (uptr, gap_size);           /* erase the record */

        if (status == MTSE_OK)                          /* if the gap write succeeded */
            uptr->pos = gap_pos;                        /*   the reposition back to the start of the gap */
        }

    else {                                              /* otherwise the read failed or is the wrong size */
        uptr->pos = gap_pos;                            /*   so restore the starting position */

        if (status != MTSE_OK)                          /* if the record was not found */
            return status;                              /*   then return the failure reason */
        else                                            /* otherwise the record is the wrong size */
            return MTSE_INVRL;                          /*   so report an invalid record length */
        }
    }

return status;                                          /* return the status of the erase operation */
}


/* Write an erase gap.

   An erase gap is written on the tape unit specified by "uptr" for the length
   specified by "gap_size" in tenths of an inch, and the status of the operation
   is returned.  The tape density must have been set by a previous
   sim_tape_set_dens call; if it has not, then no action is taken, and MTSE_FMT
   is returned.

   If the requested gap length is zero, or the tape format currently selected
   does not support erase gaps, the call succeeds with no action taken.  This
   allows a device simulator that supports writing erase gaps to use the same
   code without worrying about the tape format currently selected by the user.

   Because SIMH tape images do not carry physical parameters (e.g., recording
   density), overwriting a tape image file containing a gap is problematic if
   the density setting is not the same as that used during recording.  There is
   no way to establish a gap of a certain length unequivocally in an image file,
   so this implementation establishes a gap of a certain number of bytes that
   reflect the desired gap length at the tape density in bits per inch used
   during writing.
*/

t_stat sim_tape_wrgap (UNIT *uptr, uint32 gaplen)
{
const uint32 density = bpi [MT_DENS (uptr->dynflags)];  /* the tape density in bits per inch */
const uint32 byte_length = (gaplen * density) / 10;     /* the size of the requested gap in bytes */

if (density == 0)                                       /* if the density has not been set */
    return MTSE_FMT;                                    /*   then report a format error */
else                                                    /* otherwise */
    return tape_erase_fwd (uptr, byte_length);          /*   erase the requested gap size in bytes */
}


/* Erase a record forward.

   An erase gap is written in the forward direction on the tape unit specified
   by "uptr" for a length corresponding to a record containing the number of
   bytes specified by "bc", and the status of the operation is returned.  The
   resulting gap will occupy "bc" bytes plus the size of the record length
   metadata.  This function may be used to erase a record of length "n" in place
   by requesting a gap of length "n".  After erasure, the tape will be
   positioned at the end of the gap.

   If a length of 0 is specified, then the metadatum marker at the current tape
   position will be erased.  If the tape is not positioned at a metadatum
   marker, the routine fails with MTSE_INVRL, and the tape position is
   unchanged.
*/

t_stat sim_tape_errecf (UNIT *uptr, t_mtrlnt bc)
{
const t_mtrlnt meta_size = sizeof (t_mtrlnt);           /* the number of bytes per metadatum */
const t_mtrlnt gap_size = bc + 2 * meta_size;           /* the requested gap size in bytes */

if (bc == 0)                                            /* if a zero-length erase is requested */
    return tape_erase_fwd (uptr, meta_size);            /*   then erase a metadatum marker */
else                                                    /* otherwise */
    return tape_erase_fwd (uptr, gap_size);             /*   erase the requested gap */
}


/* Erase a record reverse.

   An erase gap is written in the reverse direction on the tape unit specified
   by "uptr" for a length corresponding to a record containing the number of
   bytes specified by "bc", and the status of the operation is returned.  The
   resulting gap will occupy "bc" bytes plus the size of the record length
   metadata.  This function may be used to erase a record of length "n" in place
   by requesting a gap of length "n".  After erasure, the tape will be
   positioned at the start of the gap.

   If a length of 0 is specified, then the metadatum marker preceding the
   current tape position will be erased.  If the tape is not positioned after a
   metadatum marker, the routine fails with MTSE_INVRL, and the tape position is
   unchanged.
*/

t_stat sim_tape_errecr (UNIT *uptr, t_mtrlnt bc)
{
const t_mtrlnt meta_size = sizeof (t_mtrlnt);           /* the number of bytes per metadatum */
const t_mtrlnt gap_size = bc + 2 * meta_size;           /* the requested gap size in bytes */

if (bc == 0)                                            /* if a zero-length erase is requested */
    return tape_erase_rev (uptr, meta_size);            /*   then erase a metadatum marker */
else                                                    /* otherwise */
    return tape_erase_rev (uptr, gap_size);             /*   erase the requested gap */
}


/* Erase a specified number of bytes.

   An erase gap is written on the tape unit specified by "uptr" for the number
   of bytes specified by "bc", and the status of the operation is returned.  No
   checking is done to preserve the tape structure while erasing, so the caller
   is responsible for ensuring that the format remains valid.

   If the requested byte count is zero, or the tape format currently selected
   does not support erase gaps, the call succeeds with no action taken.  This
   allows a device simulator that supports writing erase gaps to use the same
   code without worrying about the tape format currently selected by the user.
*/

t_stat sim_tape_erase (UNIT *uptr, t_mtrlnt bc)
{
const uint32 format = MT_GET_FMT (uptr);                /* the tape format */

MT_CLR_PNU (uptr);                                      /* clear the position-not-updated flag */

if ((uptr->flags & UNIT_ATT) == 0)                      /* if the unit is not attached */
    return MTSE_UNATT;                                  /*   then we cannot proceed */

else if (sim_tape_wrp (uptr))                           /* otherwise if the unit is write protected */
    return MTSE_WRP;                                    /*   then we cannot write */

else if (bc == 0                                        /* if the count is zero */
 || (format != MTUF_F_STD && format != MTUF_F_EXT))     /*   or gaps are not supported */
    return MTSE_OK;                                     /*     then take no action */

else                                                    /* otherwise */
    return tape_erase (uptr, bc);                       /*   erase the requested number of bytes */
}


/* Space record forward.

   Inputs:
        uptr    =       pointer to tape unit
        bc      =       pointer to returned record length or marker

   Outputs:
        status  =       operation status

   exit condition       tape position
   ------------------   -----------------------------------------------------
   unit unattached      unchanged
   read error           unchanged, PNU set if initial read
   end of file/medium   updated if a gap precedes, else unchanged and PNU set
   tape mark            updated
   other marker         updated
   tape runaway         updated
   data record          updated

   This routine is called to space over a record or metadatum marker in the
   forward direction.  On return, status is MTSE_OK if a data record, private
   marker, or reserved marker was read, or an MTSE error code if a standard
   marker (e.g. tape mark) was read, or an error occurred.  In all cases, the
   successfully read marker or data record length word is returned via the "bc"
   pointer.

   When the extended SIMH format is enabled, then the variable addressed by the
   "bc" parameter must be set on entry to a bitmap of the object classes to
   return.  Each of the classes is represented by its corresponding bit, i.e.,
   bit 0 represents class 0, bit 1 for class 1, etc.  The routine will return
   only objects from the selected classes.  Unselected class objects will be
   ignored by skipping over them until the first selected class object is seen.
   This allows a simulator to declare those classes it understands (e.g.,
   standard classes 0 and 8, plus private classes 2 and 7) and those classes it
   wishes to ignore.  Erase gap markers are always skipped, and standard markers
   are always returned, so specifying an empty bitmap will perform the
   equivalent of a "space file forward," returning only when a tape mark or
   EOM/EOF is encountered.

   When standard SIMH format is enabled, standard classes 0 and 8 are
   automatically selected, and the entry value addressed by "bc" is ignored.

   If the PNU ("position not updated") flag is set, then an error prevented a
   preceding tape read or write command from moving the tape position.  A space
   command immediately following such a failed command is assumed to be part of
   a reposition-and-retry error recovery sequence.  Because the tape was not
   actually moved, we skip the corresponding reposition here, so that the tape
   will be correctly positioned for the retry.


   Implementation notes:

    1. PNU is set by both read and write positioning failures.  A retry sequence
       would match a space reverse with a forward read, and vice versa.  We do
       not maintain separate PNUs for forward and reverse operations, so a space
       forward following a failed read forward would not move the tape.
       However, this condition is deemed so unlikely as not to warrant keeping
       direction-specific PNU flags.
*/

t_stat sim_tape_sprecf (UNIT *uptr, t_mtrlnt *bc)
{
const uint32 f = MT_GET_FMT (uptr);
t_stat       st;

if (MT_TST_PNU (uptr)) {                                /* if the PNU flag is set */
    MT_CLR_PNU (uptr);                                  /*   then clear it */

    *bc = 0;                                            /* report the record length as zero */
    return MTSE_OK;                                     /*   and return with no tape motion */
    }

st = sim_tape_rdlntf (uptr, bc);                        /* get the record length */

if (f != MTUF_F_EXT)                                    /* if the format is not extended SIMH */
    *bc = MTR_RL (*bc) & MTR_MAXLEN;                    /*   then return just the record length */

return st;
}


/* Space record reverse.

   Inputs:
        uptr    =       pointer to tape unit
        bc      =       pointer to returned record length or marker

   Outputs:
        status  =       operation status

   exit condition       tape position
   ------------------   -----------------------------------------------------
   unit unattached      unchanged
   beginning of tape    unchanged
   read error           unchanged, PNU set if initial read
   tape mark            updated
   other marker         updated
   tape runaway         updated
   data record          updated

   This routine is called to space over a record or metadatum marker in the
   reverse direction.  On return, status is MTSE_OK if a data record, private
   marker, or reserved marker was read, or an MTSE error code if a standard
   marker (e.g. tape mark) was read, or an error occurred.  In all cases, the
   successfully read marker or data record length word is returned via the "bc"
   pointer.

   See the comments for the "sim_tape_sprecr" routine above for additional
   considerations.
*/

t_stat sim_tape_sprecr (UNIT *uptr, t_mtrlnt *bc)
{
const uint32 f = MT_GET_FMT (uptr);
t_stat       st;

if (MT_TST_PNU (uptr)) {                                /* if the PNU flag is set */
    MT_CLR_PNU (uptr);                                  /*   then clear it */

    *bc = 0;                                            /* report the record length as zero */
    return MTSE_OK;                                     /*   and return with no tape motion */
    }

st = sim_tape_rdlntr (uptr, bc);                        /* get the record length */

if (f != MTUF_F_EXT)                                    /* if the format is not extended SIMH */
    *bc = MTR_RL (*bc) & MTR_MAXLEN;                    /*   then return just the record length */

return st;
}


/* Rewind tape */

t_stat sim_tape_rewind (UNIT *uptr)
{
uptr->pos = 0;
MT_CLR_PNU (uptr);
return MTSE_OK;
}

/* Reset tape */

t_stat sim_tape_reset (UNIT *uptr)
{
MT_CLR_PNU (uptr);
return MTSE_OK;
}

/* Test for BOT */

t_bool sim_tape_bot (UNIT *uptr)
{
uint32 f = MT_GET_FMT (uptr);

return (uptr->pos <= fmts[f].bot) ? TRUE : FALSE;
}

/* Test for end of tape */

t_bool sim_tape_eot (UNIT *uptr)
{
return (uptr->capac && (uptr->pos >= uptr->capac)) ? TRUE : FALSE;
}

/* Test for write protect */

t_bool sim_tape_wrp (UNIT *uptr)
{
return ((uptr->flags & MTUF_WRP) || (MT_GET_FMT (uptr) == MTUF_F_TPC)) ? TRUE : FALSE;
}

/* Process I/O error */

static t_stat sim_tape_ioerr (UNIT *uptr)
{
perror ("Magtape library I/O error");
clearerr (uptr->fileref);
return MTSE_IOERR;
}


/* Set the tape format.

   This validation routine is called to change the tape format of the unit
   addressed by "uptr".  If "desc" is NULL, then the string pointed to by "cptr"
   must contain one of the defined format names, and "val" is ignored.
   Otherwise, "desc" must point to a "uint32" variable that receives the current
   tape format code, "val" must contain one of the MTUF_F_* constants in
   sim_tape.h that specifies the new format code, and "cptr" is ignored.

   If "cptr" specifies the new format name, the "fmts" table is searched for a
   matching entry; if one is not found, SCPE_ARG is returned.  The "SIMH" name
   is used for both SIMH Standard and SIMH Extended format; which is selected
   depends on whether the unit was set for extended format when the routine was
   first entered.  A unit declared with the MT_F_EXT flag indicates that it is
   prepared to handle the extended-format calling sequence, so switching back to
   "SIMH" format from another format returns the unit to SIMH Extended format.
   In contrast, a unit originally declared with another format, including
   MT_F_STD, indicates that it is not prepared to handle the extended format, so
   switching back returns to SIMH Standard format.

   If "val" specifies the new format, this check is skipped.  This allows a
   simulator to change to SIMH Extended format temporarily, e.g., to run
   diagnostics, while ensuring that extended-format records are ignored during
   normal simulator operation.


   Implementation notes:

    1. "fmts" table entries with blank (not NULL) names correspond to format
       codes that are reserved for future use.  They are skipped automatically
       during the name search because the "cptr" string is stripped of blanks
       before entry and thus will never match a table entry with a blank name.

    2. While the "fmts" table contains entries for both SIMH Standard and SIMH
       Extended formats, both entries use the same "SIMH" name.  Therefore, only
       the first (standard format) entry will match during the search.
*/

t_stat sim_tape_set_fmt (UNIT *uptr, int32 val, char *cptr, void *desc)
{
uint32 *old_fmt = (uint32 *) desc;                      /* a pointer to receive the current format */

if (uptr == NULL)                                       /* if the unit is not supplied */
    return SCPE_IERR;                                   /*   then report an internal error */

else if (uptr->flags & UNIT_ATT)                        /* otherwise if the unit is attached */
    return SCPE_ALATT;                                  /*   then report an already attached error */

else if (desc == NULL) {                                /* otherwise if the format is set interactively */
    if (cptr == NULL || *cptr == '\0')                  /*   then if there is no format keyword */
        return SCPE_ARG;                                /*     then report an argument error */

    if (MT_GET_FMT (uptr) == MTUF_F_EXT)                /* if the unit accepts extended SIMH format */
        uptr->dynflags |= UNIT_EXTEND;                  /*   then enable changing back to that format */

    for (val = 0; val < (int32) FMT_COUNT; val++)       /* loop through the format name table */
        if (MATCH_CMD (cptr, fmts [val].name) == 0) {   /* if the name matches */
            if (val == MTUF_F_STD                       /*   then if it's SIMH format */
              && (uptr->dynflags & UNIT_EXTEND))        /*     and the unit handles extended format */
                val = MTUF_F_EXT;                       /*       then request extended format */

            uptr->flags = (uptr->flags & ~MTUF_FMT)     /*  change to the new format */
                            | fmts [val].uflags;        /*    including any required unit flags */
            return SCPE_OK;                             /*     and return success */
            }

    return SCPE_ARG;                                    /* the name is not in the format table */
    }

else                                                    /* otherwise set the format programmatically */
    if (val < 0 || val >= (int32) FMT_COUNT)            /*   then the supplied value is out of range */
        return SCPE_ARG;                                /*     then report an argument error */

    else {                                              /* otherwise the format code is valid */
        *old_fmt = MT_GET_FMT (uptr);                   /*   so return the current code */

        uptr->flags = (uptr->flags & ~MTUF_FMT)         /* change to the new format */
                        | fmts [val].uflags;            /*   including any required unit flags */
        return SCPE_OK;                                 /*     and return success */
        }
}


/* Show tape format */

t_stat sim_tape_show_fmt (FILE *st, UNIT *uptr, int32 val, void *desc)
{
int32 f = MT_GET_FMT (uptr);

if (f < (int32) FMT_COUNT && fmts [f].name [0] != ' ')
    fprintf (st, "%s format", fmts [f].name);
else
    fprintf (st, "invalid format");
return SCPE_OK;
}


/* Map a TPC format tape image */

static uint32 sim_tape_tpc_map (UNIT *uptr, t_addr *map)
{
t_addr tpos;
t_tpclnt bc;
uint32 i, objc;

if ((uptr == NULL) || (uptr->fileref == NULL))
    return 0;
for (objc = 0, tpos = 0;; ) {
    sim_fseek (uptr->fileref, tpos, SEEK_SET);
    i = sim_fread (&bc, sizeof (t_tpclnt), 1, uptr->fileref);
    if (i == 0)
        break;
    if (map)
        map[objc] = tpos;
    objc++;
    tpos = tpos + ((bc + 1) & ~1) + sizeof (t_tpclnt);
    }
if (map) map[objc] = tpos;
return objc;
}

/* Find the preceding record in a TPC file */

static t_addr sim_tape_tpc_fnd (UNIT *uptr, t_addr *map)
{
uint32 lo, hi, p;

if (map == NULL)
    return 0;
lo = 0;
hi = uptr->hwmark - 1;
do {
    p = (lo + hi) >> 1;
    if (uptr->pos == map[p])
        return ((p == 0) ? map[p] : map[p - 1]);
    else if (uptr->pos < map[p])
        hi = p - 1;
    else lo = p + 1;
    }
while (lo <= hi);
return ((p == 0) ? map[p] : map[p - 1]);
}

/* Set tape capacity */

t_stat sim_tape_set_capac (UNIT *uptr, int32 val, char *cptr, void *desc)
{
t_addr cap;
t_stat r;

if ((cptr == NULL) || (*cptr == 0))
    return SCPE_ARG;
if (uptr->flags & UNIT_ATT)
    return SCPE_ALATT;
cap = (t_addr) get_uint (cptr, 10, sim_taddr_64 ? 2000000 : 2000, &r);
if (r != SCPE_OK)
    return SCPE_ARG;
uptr->capac = cap * ((t_addr) 1000000);
return SCPE_OK;
}

/* Show tape capacity */

t_stat sim_tape_show_capac (FILE *st, UNIT *uptr, int32 val, void *desc)
{
if (uptr->capac) {
    if (uptr->capac >= (t_addr) 1000000)
        fprintf (st, "capacity=%dMB", (uint32) (uptr->capac / ((t_addr) 1000000)));
    else if (uptr->capac >= (t_addr) 1000)
        fprintf (st, "capacity=%dKB", (uint32) (uptr->capac / ((t_addr) 1000)));
    else fprintf (st, "capacity=%dB", (uint32) uptr->capac);
    }
else fprintf (st, "unlimited capacity");
return SCPE_OK;
}


/* Set the tape density.

   Set the density of the specified tape unit either to the value supplied or to
   the value represented by the supplied character string.

   If "desc" is NULL, then "val" must be set to one of the MT_DENS_* constants
   in sim_tape.h other than MT_DENS_NONE; the supplied value is used as the tape
   density, and the character string is ignored.  Otherwise, "desc" must point
   at an int32 value containing a set of allowed densities constructed as a
   bitwise OR of the appropriate MT_*_VALID values.  In this case, the string
   pointed to by "cptr" will be parsed for a decimal value corresponding to the
   desired density in bits per inch and validated against the set of allowed
   values.

   In either case, SCPE_ARG is returned if the density setting is not valid or
   allowed.  If the setting is OK, the new density is set into the unit
   structure, and SCPE_OK is returned.
*/

t_stat sim_tape_set_dens (UNIT *uptr, int32 val, char *cptr, void *desc)
{
uint32 density, new_bpi;
t_stat result = SCPE_OK;

if (uptr == NULL)                                               /* if the unit pointer is null */
    return SCPE_IERR;                                           /*   then the caller has screwed up */

else if (desc == NULL)                                          /* otherwise if a validation set was not supplied */
    if (val > 0 && val < (int32) BPI_COUNT)                     /*   then if a valid density code was supplied */
        uptr->dynflags = (uptr->dynflags & ~MTVF_DENS_MASK)     /*     then insert the code */
                           | val << UNIT_V_DF_TAPE;             /*       in the unit flags */
    else                                                        /*   otherwise the code is invalid */
        return SCPE_ARG;                                        /*     so report a bad argument */

else {                                                          /* otherwise a validation set was supplied */
    if (cptr == NULL || *cptr == 0)                             /*   but if no value is present */
        return SCPE_MISVAL;                                     /*     then report a missing value */

    new_bpi = (uint32) get_uint (cptr, 10, UINT_MAX, &result);  /* convert the string value */

    if (result != SCPE_OK)                                      /* if the conversion failed */
        result = SCPE_ARG;                                      /*   then report a bad argument */

    else for (density = 0; density < BPI_COUNT; density++)      /* otherwise validate the density */
        if (new_bpi == bpi [density]                            /* if it matches a value in the list */
          && ((1 << density) & *(int32 *) desc)) {              /*   and it's an allowed value */
            uptr->dynflags = (uptr->dynflags & ~MTVF_DENS_MASK) /*     then store the index of the value */
                               | density << UNIT_V_DF_TAPE;     /*       in the unit flags */
            return SCPE_OK;                                     /*         and return success */
            }

    result = SCPE_ARG;                                          /* if no match, then report a bad argument */
    }

return result;                                                  /* return the result of the operation */
}


/* Show the tape density */

t_stat sim_tape_show_dens (FILE *st, UNIT *uptr, int32 val, void *desc)
{
uint32 tape_density;

if (uptr == NULL)                                       /* if the unit pointer is null */
    return SCPE_IERR;                                   /*   then the caller has screwed up */

else {                                                  /* otherwise get the density */
    tape_density = bpi [MT_DENS (uptr->dynflags)];      /*   of the tape from the unit flags */

    if (tape_density)                                   /* if it's set */
        fprintf (st, "density=%d bpi", tape_density);   /*   then report it */
    else                                                /* otherwise */
        fprintf (st, "density not set");                /*   it was never set by the caller */
    }

return SCPE_OK;
}
