Skip to content

Commit

Permalink
Added tape library.
Browse files Browse the repository at this point in the history
rcornwell committed Jul 11, 2024
1 parent db692b7 commit fcdbb85
Showing 2 changed files with 2,242 additions and 0 deletions.
1,074 changes: 1,074 additions & 0 deletions util/tape/tape.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,1074 @@
/*
* S370 - Generic tape interface.
*
* Copyright 2024, Richard Cornwell
*
* 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 THE
* AUTHORS OR COPYRIGHT HOLDERS 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.
*
*/

package tape

import (
"errors"
"fmt"
"io"
"os"
)

const (
// Supported tape formats.
TapeFmtTap = 1 + iota
TapeFmtE11
TapeFmtP7B
TapeFmtAWS

// P7B constants.
p7bIRG byte = 0x80
BCDTM byte = 0x17

irgLen = 1200

// Supported densities.
Density800 = 1 + iota
Density1600
Density6250

// Currently running function.
funcNone = 0
funcRead = 1 + iota
funcWrite
funcRewind
funcReadBack
funcMark
)

var (
TapeEOT = errors.New("EOT") // End of tape error.
TapeMARK = errors.New("MARK") // Tape mark found.
TapeBOT = errors.New("BOT") // Beginning of tape.
TapeEOR = errors.New("EOR") // End of record.
TapeFORMAT = errors.New("FORMAT") // Tape format error.
TapeTYPE = errors.New("TYPE") // Tape type not supported.
)

// Structure to hold tape information.
type TapeContext struct {
file *os.File // file handle
mode int // Current input/output mode
format int // Tape format
ring bool // Has write ring
mark bool // Last record was tape mark.
bot bool // At beginning of tape
eot bool // At end of tape
seven bool // Seven track drive
frame int // Current frame
bufPos int // Position in buffer
bufLen int // Length of buffer
position int64 // Position of head of buffer in tape
lrecl uint32 // Length of current record.
recPos uint32 // Position in logical record.
startRec int64 // Start of record in tape.
dirty bool // Buffer is dirty.
buffer [32 * 1024]byte // Tape buffer.
}

// Check if tape is at load point.
func (tape *TapeContext) TapeAtLoadPt() bool {
return tape.bot
}

// Determine if tape is attached and ready.
func (tape *TapeContext) TapeReady() bool {
return tape.file != nil
}

// Determine if tape can be written.
func (tape *TapeContext) TapeRing() bool {
return tape.ring
}

// Determine if tape is 7 track or 9 track.
func (tape *TapeContext) Tape9Track() bool {
return !tape.seven
}

// Attach file to tape context.
func (tape *TapeContext) Attach(fileName string, format int, ring bool, seven bool) error {
tape.format = format
tape.seven = seven
tape.ring = ring
var err error
if ring {
tape.file, err = os.Create(fileName)
} else {
tape.file, err = os.Open(fileName)
}
tape.position = 0
tape.bot = true
tape.eot = false
tape.mark = false
tape.bufPos = 0
tape.bufLen = 0
tape.lrecl = 0
tape.startRec = 0
tape.dirty = false
return err
}

// Detach a tape file from a tape context.
func (tape *TapeContext) Detach() error {
var err error
// If buffer is dirty flush it to the file
if tape.dirty {
n := 0
_, _ = tape.file.Seek(tape.position, io.SeekStart)
n, err = tape.file.Write(tape.buffer[:tape.bufLen])
if n != tape.bufLen {
err = errors.New("Write error on: " + tape.file.Name())
}
tape.dirty = false
}
tape.file.Close()
tape.file = nil
return err
}

// Start a tape write operation.
func (tape *TapeContext) WriteStart() error {
// Error if not attached.
if tape.file == nil {
return errors.New("tape not attached")
}

// Check if tape writable.
if !tape.ring {
return errors.New("tape write protected")
}

// Clear BOT and EOT indicators.
tape.bot = false
tape.eot = false
tape.recPos = 0
tape.mode = funcWrite

// Save starting record to update later.
tape.startRec = tape.position + int64(tape.bufPos)

var err error
// Set up start of record based on tape type.
switch tape.format {
case TapeFmtTap, TapeFmtE11:
// Write dummy record length
for range 4 {
err = tape.writeNextFrame(0)
if err != nil {
break
}
}

case TapeFmtP7B:
case TapeFmtAWS:
hdr := []byte{0, 0,
byte((tape.lrecl >> 8) & 0xff),
byte(tape.lrecl & 0xff),
0xA, 0,
}
if tape.mark {
hdr[4] = 0x4
tape.mark = false
}

// Write dummy record length
for _, d := range hdr {
err = tape.writeNextFrame(d)
if err != nil {
break
}
}

default:
err = TapeTYPE
}
tape.lrecl = 0
return err
}

// Write Mark to tape.
func (tape *TapeContext) WriteMark() error {
// Error if not attached.
if tape.file == nil {
return errors.New("tape not attached")
}

// Check if tape writable.
if !tape.ring {
return errors.New("tape write protected")
}

var err error
// Clear BOT and EOT indicators.
tape.bot = false
tape.eot = false

// Save starting record to update later.
tape.startRec = tape.position + int64(tape.bufPos)

tape.recPos = 0
tape.mode = funcMark
// Set up start of record based on tape type.
switch tape.format {
case TapeFmtTap, TapeFmtE11:
// Write dummy record length
for range 4 {
err = tape.writeNextFrame(0)
if err != nil {
break
}
}
tape.lrecl = 0
case TapeFmtP7B:
err = tape.writeNextFrame(BCDTM | p7bIRG)
tape.lrecl = 0
case TapeFmtAWS:
tape.mark = true
default:
err = TapeTYPE
}
tape.frame += irgLen
return err
}

// Start a tape read forward operation.
func (tape *TapeContext) ReadForwStart() error {
// Error if not attached.
if tape.file == nil {
return errors.New("tape not attached")
}

// Clear BOT and EOT indicators.
tape.bot = false
tape.eot = false
tape.mode = funcRead

// Save starting record to update later.
tape.startRec = tape.position + int64(tape.bufPos)

// Set up start of record based on tape type.
switch tape.format {
case TapeFmtTap, TapeFmtE11:
// Read in 4 byte record length
hdr := [4]byte{}
var err error
for i := range 4 {
hdr[i], err = tape.readNextFrame()
if err != nil {
return err
}
}
tape.lrecl = uint32(hdr[0]) | (uint32(hdr[1]) << 8) |
(uint32(hdr[2]) << 16) | (uint32(hdr[3]) << 24)
if tape.lrecl == 0xffffffff {
// We hit end of tape, backup so if write we erase it.
tape.eot = true
for range 4 {
_, err = tape.readPrevFrame()
if err != nil {
return err
}
}
return TapeEOT
}

// Check for tape mark
if tape.lrecl == 0 {
tape.frame += irgLen
tape.mark = true
return TapeMARK
}

// Debug log data.
// j = tape->lrecl;
// if (j > tape->len_buff)
// j = tape->len_buff;
// k = 0;
// while (j > 0 && (tape->pos_buff + k + 16) < sizeof(tape->buffer)) {
// log_tape_s("data ");
// for(i = 0; i < j && i < 16; i++)
// log_tape_c ("%02x ", tape->buffer[tape->pos_buff + i + k]);
// log_tape_c(" ");
// for(i = 0; i < j && i < 16; i++) {
// uint8_t ch = ebcdic_to_ascii[tape->buffer[tape->pos_buff + i + k]];
// log_tape_c ("%c", isprint(ch) ? ch : '.');
// }
// j -= 16;
// k += 16;
// }

tape.recPos = 0 // Posision in logical record

case TapeFmtP7B:
// Peek at current character.
// To see if it is a tape mark.
data, err := tape.peekNextFrame()
tape.lrecl = 2
if err != nil {
return err
}

// Check if tape mark.
if data == (p7bIRG | BCDTM) {
_, _ = tape.readNextFrame()
tape.frame += irgLen
tape.mark = true
return TapeMARK
}
tape.lrecl = 0
// tape->srec = tape->pos + tape->pos_buff;
// r = tape_peek_byte(tape, &lrecl[0]);
// tape->lrecl = 2;
// if (r != 1)
// return r;
// /* If tape mark, move over it */
// if (lrecl[0] == (IRG_MASK|BCD_TM)) {
// r = tape_read_byte(tape, &lrecl[0]);
// if (r < 0)
// return r;
// tape->pos_frame += IRG_LEN;
// tape->format |= TAPE_MARK;
// log_tape("Tape mark %d\n", r);
// return (r == 0) ? 0 : 2;
// }
// tape->lrecl = 0; /* Flag at beginning of record */
// break;
case TapeFmtAWS:
// Read record header
hdr := [6]byte{}
var err error
for i := range 6 {
hdr[i], err = tape.readNextFrame()
if err != nil {
return err
}
}

tape.lrecl = (uint32(hdr[1]) << 8) | uint32(hdr[0])
fmt.Printf("Header %02x %02x\n", hdr[4], hdr[5])
default:
return TapeTYPE
}

return nil
}

// Start a tape read backword operation.
func (tape *TapeContext) ReadBackStart() error {
// Error if not attached.
if tape.file == nil {
return errors.New("tape not attached")
}

// Clear BOT and EOT indicators.
tape.bot = false
tape.eot = false
tape.mode = funcReadBack

// Save starting record to update later.
tape.startRec = tape.position + int64(tape.bufPos)

// Set up start of record based on tape type.
switch tape.format {
case TapeFmtTap, TapeFmtE11:
// Read in 4 byte record length
recLen := [4]byte{}
var err error
for i := 3; i >= 0; i-- {
recLen[i], err = tape.readPrevFrame()
if err != nil {
return err
}
}
tape.lrecl = uint32(recLen[0]) | (uint32(recLen[1]) << 8) |
(uint32(recLen[2]) << 16) | (uint32(recLen[3]) << 24)
if tape.lrecl == 0xffffffff {
// We hit end of tape, backup so if write we erase it.
tape.eot = true
return TapeEOT
}

// Check for tape mark
if tape.lrecl == 0 {
tape.frame += irgLen
tape.mark = true
return TapeMARK
}

// On tap format files odd records have 1 byte padding
if tape.format == TapeFmtTap && (tape.lrecl&1) != 0 {
_, err := tape.readPrevFrame()
if err != nil {
return err
}
}

tape.recPos = tape.lrecl
// Debug log data.
// j = tape->lrecl;
// if (j > tape->len_buff)
// j = tape->len_buff;
// k = 0;
// while (j > 0 && (tape->pos_buff + k + 16) < sizeof(tape->buffer)) {
// log_tape_s("data ");
// for(i = 0; i < j && i < 16; i++)
// log_tape_c ("%02x ", tape->buffer[tape->pos_buff + i + k]);
// log_tape_c(" ");
// for(i = 0; i < j && i < 16; i++) {
// uint8_t ch = ebcdic_to_ascii[tape->buffer[tape->pos_buff + i + k]];
// log_tape_c ("%c", isprint(ch) ? ch : '.');
// }
// j -= 16;
// k += 16;
// }

case TapeFmtP7B:
// Peek at current character.
// To see if it is a tape mark.
tape.startRec = tape.position + int64(tape.bufPos)
data, err := tape.readPrevFrame()
if err != nil {
return err
}

tape.lrecl = 0
// Check if tape mark.
if data == (p7bIRG | BCDTM) {
tape.startRec = tape.position + int64(tape.bufPos)
tape.frame -= irgLen
tape.lrecl = 2
tape.mark = true
return TapeMARK
}
// Reposition to start of next record.
_, err2 := tape.readNextFrame()
return err2

case TapeFmtAWS:
// Read record header
hdr := [6]byte{}
var err error
for i := 5; i >= 0; i-- {
hdr[i], err = tape.readPrevFrame()
if err != nil {
return err
}
}

tape.lrecl = (uint32(hdr[3]) << 8) | uint32(hdr[2])
fmt.Printf("Header %02x %02x\n", hdr[4], hdr[5])
default:
return TapeTYPE
}

return nil
}

// Read one frame from tape.
func (tape *TapeContext) ReadFrame() (byte, error) {
// Error if not attached.
if tape.file == nil {
return 0, errors.New("tape not attached")
}

if tape.mark {
return 0, TapeMARK
}

var err error
var data byte
switch tape.format {
case TapeFmtTap, TapeFmtE11:
switch tape.mode {
case funcRead:
if tape.recPos == tape.lrecl {
return 0, TapeEOR
}
data, err = tape.readNextFrame()
tape.recPos++
tape.frame++
case funcReadBack:
if tape.recPos == 0 {
return 0, TapeEOR
}
data, err = tape.readPrevFrame()
tape.recPos--
tape.frame--
}

case TapeFmtP7B:
switch tape.mode {
case funcRead:
if tape.lrecl == 2 {
return 0, TapeEOR
}
data, err = tape.readNextFrame()
if tape.lrecl == 1 && (data&p7bIRG) != 0 {
_, _ = tape.readPrevFrame()
tape.lrecl = 2
return 0, TapeEOR
} else {
tape.lrecl = 1
}
tape.frame++
data &= ^p7bIRG
case funcReadBack:
if tape.lrecl == 2 {
return 0, TapeEOR
}
data, err = tape.readPrevFrame()
if tape.lrecl == 1 && (data&p7bIRG) != 0 {
tape.lrecl = 2
} else {
tape.lrecl = 1
}
data &= ^p7bIRG
tape.frame--
}
case TapeFmtAWS:
switch tape.mode {
case funcRead:
if tape.recPos == tape.lrecl {
return 0, TapeEOR
}
data, err = tape.readNextFrame()
tape.recPos++
tape.frame++
case funcReadBack:
if tape.recPos == 0 {
return 0, TapeEOR
}
data, err = tape.readPrevFrame()
tape.recPos--
tape.frame--
}

default:
return 0, TapeTYPE
}
return data, err
}

// Write one frame to tape.
func (tape *TapeContext) WriteFrame(data byte) error {
// Error if not attached.
if tape.file == nil {
return errors.New("tape not attached")
}

// For P7B format for begin set IRG flag.
if tape.format == TapeFmtP7B {
data &= ^p7bIRG
if tape.recPos == 0 {
data |= p7bIRG
}
}
tape.lrecl++
tape.frame++
tape.recPos++
return tape.writeNextFrame(data)
}

// Finsh a record.
func (tape *TapeContext) FinishRecord() error {
// Error if not attached.
if tape.file == nil {
return errors.New("tape not attached")
}

// If there was tape mark, nothing more to do.
if tape.mark {
tape.mark = false
return nil
}

var err error
// Finish record
switch tape.format {
case TapeFmtTap, TapeFmtE11:
err = tape.finishTAPfunc()
case TapeFmtP7B:
switch tape.mode {
case funcRead, funcReadBack:
for tape.lrecl != 2 {
_, err = tape.ReadFrame()
if errors.Is(err, TapeEOR) {
return nil
}
if err != nil {
break
}
}
}
case TapeFmtAWS:
err = tape.finishAWSfunc()
default:
return TapeTYPE
}
tape.mode = funcNone
return err
}

// Start rewind.
func (tape *TapeContext) StartRewind() error {
// Error if not attached.
if tape.file == nil {
return errors.New("tape not attached")
}

// If buffer dirty, flush it to file.
if tape.dirty {
_, _ = tape.file.Seek(tape.position, io.SeekStart)
n, err := tape.file.Write(tape.buffer[:tape.bufLen])
if err != nil {
return err
}
if n != tape.bufLen {
return errors.New("Write error on: " + tape.file.Name())
}
tape.dirty = false
}
tape.bufPos = 0
tape.bufLen = 0
return nil
}

// Rewind tape by number of frames.
func (tape *TapeContext) RewindFrames(frames int) bool {
// If we hit beginning of tape set position to zero.
if tape.frame < frames {
tape.frame = 0
tape.position = 0
tape.mark = false
tape.eot = false
tape.bot = true
return true
}
tape.frame -= frames
return false
}

func NewTapeContext() *TapeContext {
return &TapeContext{}
}

// Finish TAP operations.
func (tape *TapeContext) finishTAPfunc() error {
switch tape.mode {
case funcRead:
// Make sure we read all of record.
for tape.recPos < tape.lrecl {
_, err := tape.readNextFrame()
if err != nil {
return err
}
tape.recPos++
}

// If Tap format and odd record length skip padding
if tape.format == TapeFmtTap && (tape.lrecl&1) != 0 {
_, err := tape.readNextFrame()
if err != nil {
return err
}
}

recLen := [4]byte{}
var err error
for i := range 4 {
recLen[i], err = tape.readNextFrame()
if err != nil {
return err
}
}
lrecl := uint32(recLen[0]) | (uint32(recLen[1]) << 8) |
(uint32(recLen[2]) << 16) | (uint32(recLen[3]) << 24)
if lrecl != tape.lrecl {
return TapeFORMAT
}

case funcWrite:
// If TAP format, and odd record insert pad character
if tape.format == TapeFmtTap && (tape.lrecl&1) != 0 {
_ = tape.writeNextFrame(0)
}

lrecl := [4]byte{
byte(tape.lrecl & 0xff),
byte((tape.lrecl >> 8) & 0xff),
byte((tape.lrecl >> 16) & 0xff),
byte((tape.lrecl >> 24) & 0xff),
}

// Write ending and beginning record length
for _, d := range lrecl {
err := tape.writePrevByte(d)
if err != nil {
return err
}

err = tape.writeNextFrame(d)
if err != nil {
return err
}
}

case funcReadBack:
// Read rest of record if still data.
for tape.recPos > 0 {
_, err := tape.readPrevFrame()
if err != nil {
return err
}
tape.recPos--
}

// Read in header.
recLen := [4]byte{}
for i := 3; i >= 0; i-- {
var err error
recLen[i], err = tape.readPrevFrame()
// fmt.Printf(" F: %d %02x\n", i, recLen[i])
if err != nil {
return err
}
}

// Make sure header matches
lrecl := uint32(recLen[0]) | (uint32(recLen[1]) << 8) |
(uint32(recLen[2]) << 16) | (uint32(recLen[3]) << 24)
if lrecl != tape.lrecl {
return TapeFORMAT
}
}
return nil
}

// Finish TAP operations.
func (tape *TapeContext) finishAWSfunc() error {
switch tape.mode {
case funcRead:
// Make sure we read all of record.
for tape.recPos < tape.lrecl {
_, err := tape.readNextFrame()
if err != nil {
return err
}
tape.recPos++
}

// Read record header
hdr := [6]byte{}
var err error
for i := range 6 {
hdr[i], err = tape.readNextFrame()
if err != nil {
return err
}
}

lrecl := (uint32(hdr[3]) << 8) | uint32(hdr[2])
fmt.Printf("Header %02x %02x\n", hdr[4], hdr[5])
if lrecl != tape.lrecl {
return TapeFORMAT
}

// Check if tape mark.
if hdr[4] == 0x4 {
tape.mark = true
}

case funcWrite:
// If TAP format, and odd record insert pad character
if tape.format == TapeFmtTap && (tape.lrecl&1) != 0 {
_ = tape.writeNextFrame(0)
}

lrecl := [4]byte{
byte(tape.lrecl & 0xff),
byte((tape.lrecl >> 8) & 0xff),
byte((tape.lrecl >> 16) & 0xff),
byte((tape.lrecl >> 24) & 0xff),
}

// Write ending and beginning record length
for _, d := range lrecl {
err := tape.writePrevByte(d)
if err != nil {
return err
}

err = tape.writeNextFrame(d)
if err != nil {
return err
}
}

case funcReadBack:
// Make sure we read all of record.
for tape.recPos < tape.lrecl {
_, err := tape.readNextFrame()
if err != nil {
return err
}
tape.recPos++
}

// Read record header
hdr := [6]byte{}
var err error
for i := range 6 {
hdr[i], err = tape.readNextFrame()
if err != nil {
return err
}
}

lrecl := (uint32(hdr[1]) << 8) | uint32(hdr[0])
fmt.Printf("Header %02x %02x\n", hdr[4], hdr[5])
if lrecl != tape.lrecl {
return TapeFORMAT
}

// Check if tape mark.
if hdr[4] == 0x4 {
tape.mark = true
}
}
return nil
}

// Read next frame from tape buffer.
func (tape *TapeContext) readNextFrame() (byte, error) {
if tape.file == nil {
return 0, errors.New("tape not attached")
}
// Check if at end of buffer
err := tape.flushBuffer()
if err != nil {
return 0, err
}
err = tape.readBuffer()
if err != nil {
return 0, err
}
data := tape.buffer[tape.bufPos]
tape.bufPos++
return data, nil
}

// Peek at next frame from tape buffer.
func (tape *TapeContext) peekNextFrame() (byte, error) {
if tape.file == nil {
return 0, errors.New("tape not attached")
}
// Check if at end of buffer
err := tape.flushBuffer()
if err != nil {
return 0, err
}
err = tape.readBuffer()
if err != nil {
return 0, err
}
data := tape.buffer[tape.bufPos]
return data, nil
}

// Write character to tape.
func (tape *TapeContext) writeNextFrame(data byte) error {
if tape.file == nil {
return errors.New("tape not attached")
}

// Check if at end of buffer
if tape.bufPos >= len(tape.buffer) {
// If buffer is dirty flush it to the file
if tape.dirty {
_, _ = tape.file.Seek(tape.position, io.SeekStart)
n, err := tape.file.Write(tape.buffer[:])
if err != nil {
return err
}
if n != tape.bufLen {
return errors.New("Write error on: " + tape.file.Name())
}
tape.position += int64(tape.bufLen)
tape.dirty = false
}
tape.bufLen = 0
tape.bufPos = 0
}

// Save data and advance position in buffer
tape.buffer[tape.bufPos] = data
tape.bufPos++
tape.dirty = true

// Adjust length of buffer if more data
if tape.bufPos > tape.bufLen {
tape.bufLen = tape.bufPos
}
return nil
}

// Write to previous tape byte.
func (tape *TapeContext) writePrevByte(data byte) error {
if tape.file == nil {
return errors.New("tape not attached")
}

pos := tape.startRec - tape.position
// Check within buffer
// fmt.Printf("Write previous %d[%d], pos=%d, %02x\n", tape.bufPos, tape.bufLen, pos, data)
if pos >= 0 && pos < int64(tape.bufLen) {
tape.buffer[pos] = data
// if pos > int64(tape.bufLen) {
// tape.bufLen = int(pos)
// }
tape.dirty = true
} else {
_, _ = tape.file.Seek(tape.startRec, io.SeekStart)
_, err := tape.file.Write([]byte{data})
if err != nil {
return err
}
}
tape.startRec++
return nil
}

// Flush a buffer and read in new one if needed.
func (tape *TapeContext) flushBuffer() error {
if tape.bufPos < tape.bufLen {
return nil
}
// If buffer is dirty flush it to the file
if tape.dirty {
_, _ = tape.file.Seek(tape.position, io.SeekStart)
n, err := tape.file.Write(tape.buffer[:tape.bufLen])
if err != nil {
return err
}
if n != tape.bufLen {
return errors.New("Write error on: " + tape.file.Name())
}
tape.position += int64(tape.bufLen)
tape.bufLen = 0
tape.dirty = false
}

return nil
}

// Read in buffer.
func (tape *TapeContext) readBuffer() error {
if tape.bufPos < tape.bufLen {
return nil
}
var err error
// Advance tape by size of buffer
tape.position += int64(tape.bufLen)
_, _ = tape.file.Seek(tape.position, io.SeekStart)
tape.bufLen, err = tape.file.Read(tape.buffer[:])
tape.bufPos = 0
if errors.Is(err, io.EOF) {
tape.eot = true
}
return err
}

// Read previous frame from tape.
func (tape *TapeContext) readPrevFrame() (byte, error) {
if tape.file == nil {
return 0, errors.New("tape not attached")
}

// Check if not at beginning buffer or buffer not empty
if tape.bufPos != 0 && tape.bufLen != 0 {
tape.bufPos--
data := tape.buffer[tape.bufPos]
return data, nil
}

// If buffer is dirty flush it to the file
if tape.dirty {
_, _ = tape.file.Seek(tape.position, io.SeekStart)
n, err := tape.file.Write(tape.buffer[:tape.bufLen])
if err != nil {
return 0, err
}
if n != tape.bufLen {
return 0, errors.New("Write error on: " + tape.file.Name())
}
tape.dirty = false
}

// If at beging of tape, return BOT status.
if tape.bot {
fmt.Println("At BOT")
return 0, TapeBOT
}

// If at beging of file, set BOT.
if tape.position == 0 {
data := tape.buffer[tape.bufPos]
tape.bot = true
tape.bufPos = 0
tape.bufLen = 0
return data, TapeBOT
}

// Backup current buffer first position
// Check if moved before start of tape.
opos := -1
if int(tape.position) < len(tape.buffer) {
opos = int(tape.position)
tape.position = 0
} else {
tape.position -= int64(len(tape.buffer))
}

// Fill buffer.
_, _ = tape.file.Seek(tape.position, io.SeekStart)
n, err := tape.file.Read(tape.buffer[:])
tape.bufLen = n
if err != nil {
return 0, err
}

if opos == -1 {
tape.bufPos = tape.bufLen
} else {
tape.bufPos = 0
}

tape.eot = false
tape.bufPos--
data := tape.buffer[tape.bufPos]
return data, nil
}
1,168 changes: 1,168 additions & 0 deletions util/tape/tape_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,1168 @@
/*
* S370 - Tape emulation test cases.
*
* Copyright 2022, Richard Cornwell
*
* 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 THE
* AUTHORS OR COPYRIGHT HOLDERS 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.
*
*/

package tape

import (
"errors"
"fmt"
"io"
"os"
"testing"
)

var (
fileTap string
fileE11 string
fileP7B string
fileTemp string
ctx *TapeContext
)

// Write out tape block
func writeBlock(file *os.File, buffer []byte, format int) {
recl := len(buffer)
length := recl

switch format {
case TapeFmtTap:
if (length & 1) != 0 {
buffer = append(buffer, 0)
}
fallthrough
case TapeFmtE11:
lrecl := []byte{
byte(recl & 0xff),
byte((recl >> 8) & 0xff),
byte((recl >> 16) & 0xff),
byte((recl >> 24) & 0xff),
}
_, _ = file.Write(lrecl)
_, _ = file.Write(buffer)
_, _ = file.Write(lrecl)
case TapeFmtP7B:
// Put IRG on head of record.
buffer[0] |= p7bIRG
_, _ = file.Write(buffer)
}
}

// Write a tape mark.
func writeMark(file *os.File, format int) {
switch format {
case TapeFmtTap, TapeFmtE11:
lrecl := []byte{0, 0, 0, 0}
_, _ = file.Write(lrecl)
case TapeFmtP7B:
// Put IRG on head of record.
rec := []byte{BCDTM | p7bIRG}
_, _ = file.Write(rec)
}
}

// Create tape file.
func createTapeFile(file *os.File, recs int, format int) {
// Write bunch of odd recordds.
for i := range recs {
rec := fmt.Sprintf("%05d ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789X", i)
writeBlock(file, []byte(rec), format)
}
writeMark(file, format)
// Write bunch of even records.
for i := range recs {
rec := fmt.Sprintf("%05d ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789XY", i+recs)
writeBlock(file, []byte(rec), format)
}
writeMark(file, format)
file.Close()
}

// Creat test tape files
func setupTape() error {
f, err := os.CreateTemp("", "tapeP7B")
if err != nil {
return err
}
fileP7B = f.Name()
createTapeFile(f, 100, TapeFmtP7B)
f.Close()

f, err = os.CreateTemp("", "tapeTAP")
if err != nil {
return err
}
fileTap = f.Name()
createTapeFile(f, 100, TapeFmtTap)
f.Close()

f, err = os.CreateTemp("", "tapeE11")
if err != nil {
return err
}
fileE11 = f.Name()
createTapeFile(f, 100, TapeFmtE11)
f.Close()
return nil
}

func freeCTX() {
if ctx == nil {
return
}
if ctx.file != nil {
ctx.file.Close()
}
}

func cleanup() {
freeCTX()
os.Remove(fileP7B)
os.Remove(fileTap)
os.Remove(fileE11)
}

// Check that we can attach to a tape.
func TestAttach(t *testing.T) {
ctx = NewTapeContext()
defer cleanup()
err := setupTape()
if err != nil {
t.Error(err)
return
}

err = ctx.Attach(fileTap, TapeFmtTap, false, false)
if err != nil {
t.Error(err)
}
if !ctx.TapeAtLoadPt() {
t.Error("Tap format not at load point")
}
_ = ctx.Detach()

err = ctx.Attach(fileTap, TapeFmtE11, false, false)
if err != nil {
t.Error(err)
}
if !ctx.TapeAtLoadPt() {
t.Error("E11 format not at load point")
}
_ = ctx.Detach()

err = ctx.Attach(fileTap, TapeFmtP7B, false, false)
if err != nil {
t.Error(err)
}
if !ctx.TapeAtLoadPt() {
t.Error("P7B format not at load point")
}
_ = ctx.Detach()
cleanup()
err = ctx.Attach(fileTap, TapeFmtE11, false, false)
if !errors.Is(err, os.ErrNotExist) {
t.Error(err)
}
}

// Read test of tape.
func testRead(fileName string, fmtStr string, format int, t *testing.T) {
ctx = NewTapeContext()
err := ctx.Attach(fileName, format, false, false)
if err != nil {
t.Error(err)
}
defer ctx.Detach()
if !ctx.TapeAtLoadPt() {
t.Error(fmtStr + " format not at load point")
}
if ctx.TapeRing() {
t.Error(fmtStr + " not write protect")
}

rec := 0
mark := false
for !mark {
err = ctx.ReadForwStart()
if err != nil {
if !errors.Is(err, TapeMARK) {
t.Error(err)
} else {
_ = ctx.FinishRecord()
mark = true
}
break
}

buffer := []byte{}
for {
var data byte
data, err = ctx.ReadFrame()
if err != nil {
if !errors.Is(err, TapeEOR) {
t.Error(err)
}
break
}
buffer = append(buffer, data)
}

err = ctx.FinishRecord()
if errors.Is(err, TapeMARK) {
mark = true
break
}

testRec := fmt.Sprintf("%05d ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789X", rec)
if string(buffer) != testRec {
t.Error(fmtStr + "Read failed go: " + string(buffer))
t.Error(fmtStr + "Expected: " + testRec)
}

if ctx.TapeAtLoadPt() {
t.Error(fmtStr + " format at load point")
break
}
rec++
}

if !mark {
t.Error(fmtStr + " Mark not found")
}

if rec != 100 {
t.Errorf("%s Got %d records, expected: 100", fmtStr, rec)
}

mark = false
for !mark {
err = ctx.ReadForwStart()
if err != nil {
if !errors.Is(err, TapeMARK) {
t.Error(err)
} else {
_ = ctx.FinishRecord()
mark = true
}
break
}

buffer := []byte{}
for {
var data byte
data, err = ctx.ReadFrame()
if err != nil {
if !errors.Is(err, TapeEOR) {
t.Error(err)
}
break
}
buffer = append(buffer, data)
}

err = ctx.FinishRecord()
if errors.Is(err, TapeMARK) {
mark = true
break
}

testRec := fmt.Sprintf("%05d ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789XY", rec)
if string(buffer) != testRec {
t.Errorf("%s Read failed: %s", fmtStr, string(buffer))
}

if ctx.TapeAtLoadPt() {
t.Error(fmtStr + " format at load point")
break
}
rec++
}

if !mark {
t.Error(fmtStr + " Mark not found")
}

if rec != 200 {
t.Errorf("%s Got %d records, expected: 100", fmtStr, rec)
}
}

// Test read of E11 tape.
func TestReadE11(t *testing.T) {
defer cleanup()
err := setupTape()
if err != nil {
t.Error(err)
return
}

testRead(fileE11, "E11", TapeFmtE11, t)
}

// Test read of TAP tape.
func TestReadTap(t *testing.T) {
defer cleanup()
err := setupTape()
if err != nil {
t.Error(err)
return
}

testRead(fileTap, "Tap", TapeFmtTap, t)
}

// Test read of P7B tape.
func TestReadP7B(t *testing.T) {
defer cleanup()
err := setupTape()
if err != nil {
t.Error(err)
return
}

testRead(fileP7B, "P7B", TapeFmtP7B, t)
}

// Read test of tape.
func testWrite(fmtStr string, format int, t *testing.T) {
ctx = NewTapeContext()
f, err := os.CreateTemp("", "tapeFile")
if err != nil {
return
}
fileTemp = f.Name()
f.Close()
defer os.Remove(fileTemp)

mark := false
err = ctx.Attach(fileTemp, format, true, false)
if err != nil {
t.Error(err)
}
defer ctx.Detach()
if !ctx.TapeAtLoadPt() {
t.Error(fmtStr + " format not at load point")
}
if !ctx.TapeRing() {
t.Error(fmtStr + " is write protect")
}

rec := 0
sz := 0
// Write a long enough tape to run over a couple buffers
for sz < (80 * 1024) {
err = ctx.WriteStart()
if err != nil {
t.Error(err)
break
}

testRec := fmt.Sprintf("%05d ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789X", rec)
for _, data := range testRec {
err = ctx.WriteFrame(byte(data))
if err != nil {
t.Error(err)
break
}
sz++
}

err = ctx.FinishRecord()
if err != nil {
t.Error(err)
break
}
rec++
}

// Write tape mark.
err = ctx.WriteMark()
if err != nil {
t.Error(err)
}

// Rewind tape to beginning
err = ctx.StartRewind()
if err != nil {
t.Error(err)
}

for !ctx.RewindFrames(10000) {
}

if !ctx.TapeAtLoadPt() {
t.Error(fmtStr + " format not at load point")
}
readSz := 0
readRec := 0

mark = false
for !mark {
err = ctx.ReadForwStart()
if err != nil {
if !errors.Is(err, TapeMARK) {
t.Error(err)
} else {
mark = true
}
break
}
buffer := []byte{}
for {
var data byte
data, err = ctx.ReadFrame()
if err != nil {
if !errors.Is(err, TapeEOR) {
t.Error(err)
}
break
}
buffer = append(buffer, data)
readSz++
}

err = ctx.FinishRecord()
if errors.Is(err, TapeMARK) {
mark = true
break
}

testRec := fmt.Sprintf("%05d ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789X", readRec)
if string(buffer) != testRec {
t.Errorf(fmtStr+" Read failed: %s", string(buffer))
}

if ctx.TapeAtLoadPt() {
t.Error(fmtStr + " format at load point")
break
}
readRec++
}

if !mark {
t.Error(fmtStr + " Mark not found")
}

if rec != readRec {
t.Errorf(fmtStr+" Got %d records, expected: %d", readRec, rec)
}
}

// Test write of E11 tape.
func TestWriteE11(t *testing.T) {
testWrite("E11", TapeFmtE11, t)
}

// Test write of Tap tape.
func TestWriteTap(t *testing.T) {
testWrite("Tap", TapeFmtTap, t)
}

// Test write of P7B tape.
func TestWriteP7B(t *testing.T) {
testWrite("P7B", TapeFmtP7B, t)
}

// Write a series of records of increasing size.
func testWriteLong(fmtStr string, format int, t *testing.T) {
ctx = NewTapeContext()
f, err := os.CreateTemp("", "tapeFile")
if err != nil {
return
}
fileTemp = f.Name()
f.Close()
defer os.Remove(fileTemp)

mark := false
err = ctx.Attach(fileTemp, format, true, false)
if err != nil {
t.Error(err)
}
defer ctx.Detach()
if !ctx.TapeAtLoadPt() {
t.Error(fmtStr + " format not at load point")
}
if !ctx.TapeRing() {
t.Error(fmtStr + " is write protect")
}

rec := 0
sz := 2000
// Write a long enough tape to run over a couple buffers
for sz < (80 * 1024) {
err = ctx.WriteStart()
if err != nil {
t.Error(err)
break
}

for i := range sz {
data := (i & 0xff)
err = ctx.WriteFrame(byte(data))
if err != nil {
t.Error(err)
break
}
}

err = ctx.FinishRecord()
if err != nil {
t.Error(err)
break
}
sz += 2000
rec++
}

// Write tape mark.
err = ctx.WriteMark()
if err != nil {
t.Error(err)
}

// Rewind tape to beginning
err = ctx.StartRewind()
if err != nil {
t.Error(err)
}

done := false
for !done {
done = ctx.RewindFrames(10000)
}

if !ctx.TapeAtLoadPt() {
t.Error(fmtStr + " format not at load point")
}

readSz := 2000
readRec := 0

// Make sure we can read them back.
mark = false
for !mark {
err = ctx.ReadForwStart()
if err != nil {
if !errors.Is(err, TapeMARK) {
t.Error(err)
} else {
mark = true
}
break
}

i := 0
fail := false
for {
var data byte
data, err = ctx.ReadFrame()
if err != nil {
if !errors.Is(err, TapeEOR) {
t.Error(err)
}
break
}
if format == TapeFmtP7B {
if data != byte(i&0x7f) {
fail = true
}
} else {
if data != byte(i&0xff) {
fail = true
}
}
i++
}

err = ctx.FinishRecord()
if errors.Is(err, TapeMARK) {
mark = true
break
}

if fail {
t.Error(fmtStr + " record did not match")
}

if i != readSz {
t.Errorf(fmtStr+" Got %d wrong size, expected: %d", readSz, i)
}

if ctx.TapeAtLoadPt() {
t.Error(fmtStr + " format at load point")
break
}
readRec++
readSz += 2000
}

if !mark {
t.Error(fmtStr + " Mark not found")
}

if rec != readRec {
t.Errorf(fmtStr+" Got %d records, expected: %d", readRec, rec)
}
}

// Write a series of record in increasing size.
func TestWriteLongE11(t *testing.T) {
testWriteLong("E11", TapeFmtE11, t)
}

// Write a series of record in increasing size.
func TestWriteLongTap(t *testing.T) {
testWriteLong("Tap", TapeFmtTap, t)
}

// Write a series of record in increasing size.
func TestWriteLongP7B(t *testing.T) {
testWriteLong("P7B", TapeFmtP7B, t)
}

// Write a tape mark ever 10 records, verify read correct forward and backwards.
func testMark(fmtStr string, format int, t *testing.T) {
ctx = NewTapeContext()
f, err := os.CreateTemp("", "tapeFile")
if err != nil {
return
}
fileTemp = f.Name()
f.Close()
defer os.Remove(fileTemp)

err = ctx.Attach(fileTemp, format, true, false)
if err != nil {
t.Error(err)
}
defer ctx.Detach()
if !ctx.TapeAtLoadPt() {
t.Error(fmtStr + " format not at load point")
}
if !ctx.TapeRing() {
t.Error(fmtStr + " is write protect")
}

rec := 0
// Every 10 records write a tape mark, then put 2 on end
for rec <= 200 {
err = ctx.WriteStart()
if err != nil {
t.Error(err)
break
}

testRec := fmt.Sprintf("%05d ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789X", rec)
if rec > 100 {
testRec += "Y"
}
for _, data := range testRec {
err = ctx.WriteFrame(byte(data))
if err != nil {
t.Error(err)
break
}
}

err = ctx.FinishRecord()
if err != nil {
t.Error(err)
break
}

if (rec % 10) == 0 {
err = ctx.WriteMark()
if err != nil {
t.Error(err)
break
}
}
rec++
}

// Write tape mark.
err = ctx.WriteMark()
if err != nil {
t.Error(err)
}

// Rewind tape to beginning
err = ctx.StartRewind()
if err != nil {
t.Error(err)
}

done := false
for !done {
done = ctx.RewindFrames(10000)
}

if !ctx.TapeAtLoadPt() {
t.Error(fmtStr + " format not at load point")
}

readRec := 0
mark := 0

// Make sure we can read them back.
for {
err = ctx.ReadForwStart()

// Check if tape Mark
if errors.Is(err, TapeMARK) {
mark++
err = ctx.FinishRecord()
if err != nil {
t.Error(err)
continue
}
err = ctx.ReadForwStart()
if errors.Is(err, TapeMARK) {
break
}
if (readRec % 10) != 1 {
t.Errorf(fmtStr+" Mark not correct: %d", readRec)
}
}

if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Error(err)
}
buffer := []byte{}
for {
var data byte
data, err = ctx.ReadFrame()
if err != nil {
if !errors.Is(err, TapeEOR) {
t.Error(err)
}
break
}
buffer = append(buffer, data)
}

err = ctx.FinishRecord()
if err != nil {
t.Error(err)
}

testRec := fmt.Sprintf("%05d ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789X", readRec)
if readRec > 100 {
testRec += "Y"
}
if string(buffer) != testRec {
t.Error(fmtStr + " Expected: " + testRec)
t.Error(fmtStr + " Read failed: " + string(buffer))
}

if ctx.TapeAtLoadPt() {
t.Error(fmtStr + " format at load point")
break
}
readRec++
}

if mark != 21 {
t.Errorf(fmtStr+" Mark count wrong %d", mark)
}

if rec != readRec {
t.Errorf(fmtStr+" Got %d records, expected: %d", readRec, rec)
}

// Skip double MARK
err = ctx.ReadBackStart()

// Check if tape Mark
if !errors.Is(err, TapeMARK) {
t.Error(err)
}
err = ctx.FinishRecord()
if err != nil {
t.Error(err)
}

// Make sure we can read backward
for !ctx.TapeAtLoadPt() {
err = ctx.ReadBackStart()

// Check if tape Mark
if errors.Is(err, TapeMARK) {
mark--
err = ctx.FinishRecord()
if err != nil {
t.Error(err)
continue
}
_ = ctx.ReadBackStart()
if (readRec % 10) != 1 {
t.Errorf(fmtStr+" Mark not correct: %d", readRec)
}
} else if errors.Is(err, TapeBOT) {
break
} else if err != nil {
t.Error(err)
}

readRec--
testRec := fmt.Sprintf("%05d ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789X", readRec)
if readRec > 100 {
testRec += "Y"
}
l := len(testRec)
buffer := make([]byte, l)

for l > 0 {
var data byte
data, err = ctx.ReadFrame()
if err != nil {
if !errors.Is(err, TapeEOR) {
t.Error(err)
}
break
}
l--
buffer[l] = data
}

err = ctx.FinishRecord()
if err != nil {
t.Error(err)
}

if string(buffer) != testRec {
t.Error(fmtStr + " Expected: " + testRec)
t.Errorf(fmtStr + " Read failed: " + string(buffer))
}
}

if mark != 0 {
t.Errorf(fmtStr+" Mark count wrong %d", mark)
}

if readRec != 0 {
t.Errorf(fmtStr+" Got %d records, expected: %d", rec-readRec, rec)
}
}

// Write a tape mark ever 10 records, verify read correct forward and backwards.
func TestWMarkE11(t *testing.T) {
testMark("E11", TapeFmtE11, t)
}

// Write a tape mark ever 10 records, verify read correct forward and backwards.
func TestWMarkTAP(t *testing.T) {
testMark("TAP", TapeFmtTap, t)
}

// Write a tape mark ever 10 records, verify read correct forward and backwards.
func TestWMarkP7B(t *testing.T) {
testMark("P7B", TapeFmtP7B, t)
}

// Write a tape mark ever 10 records, verify read correct forward and backwards.
func testShortRead(fmtStr string, format int, t *testing.T) {
ctx = NewTapeContext()
f, err := os.CreateTemp("", "tapeFile")
if err != nil {
return
}
fileTemp = f.Name()
f.Close()
defer os.Remove(fileTemp)

err = ctx.Attach(fileTemp, format, true, false)
if err != nil {
t.Error(err)
}
defer ctx.Detach()
if !ctx.TapeAtLoadPt() {
t.Error(fmtStr + " format not at load point")
}
if !ctx.TapeRing() {
t.Error(fmtStr + " is write protect")
}

rec := 0
// Write out 200 records 100 odd/100 even.
for rec <= 200 {
err = ctx.WriteStart()
if err != nil {
t.Error(err)
break
}

testRec := fmt.Sprintf("%05d ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789X", rec)
if rec > 100 {
testRec += "Y"
}
for _, data := range testRec {
err = ctx.WriteFrame(byte(data))
if err != nil {
t.Error(err)
break
}
}

err = ctx.FinishRecord()
if err != nil {
t.Error(err)
break
}
rec++
}

// Write tape mark.
err = ctx.WriteMark()
if err != nil {
t.Error(err)
}

// Rewind tape to beginning
err = ctx.StartRewind()
if err != nil {
t.Error(err)
}

done := false
for !done {
done = ctx.RewindFrames(10000)
}

if !ctx.TapeAtLoadPt() {
t.Error(fmtStr + " format not at load point")
}

readRec := 0
mark := false

length := 79
// Make sure we can read them back.
for !mark {
err = ctx.ReadForwStart()

if err != nil {
if !errors.Is(err, TapeMARK) {
t.Error(err)
} else {
mark = true
}
break
}

if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Error(err)
}
buffer := []byte{}
for range length {
var data byte
data, err = ctx.ReadFrame()
if err != nil {
if !errors.Is(err, TapeEOR) {
t.Error(err)
}
break
}
buffer = append(buffer, data)
}

err = ctx.FinishRecord()
if err != nil {
t.Error(err)
}

testRec := fmt.Sprintf("%05d ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789X", readRec)
if readRec > 100 {
testRec += "Y"
}
if string(buffer) != testRec[:length] {
t.Error(fmtStr + " Expected: " + testRec[:length])
t.Error(fmtStr + " Read failed: " + string(buffer))
}

length--
if length < 0 {
length = 79
if readRec > 100 {
length++
}
}
if ctx.TapeAtLoadPt() {
t.Error(fmtStr + " format at load point")
break
}
readRec++
}

if rec != readRec {
t.Errorf(fmtStr+" Got %d records, expected: %d", readRec, rec)
}

// Skip double MARK
err = ctx.ReadBackStart()

// Check if tape Mark
if !errors.Is(err, TapeMARK) {
t.Error(err)
}
err = ctx.FinishRecord()
if err != nil {
t.Error(err)
}

length = 80
// Make sure we can read backward
for !ctx.TapeAtLoadPt() {
err = ctx.ReadBackStart()

// Check if BOT
if errors.Is(err, TapeBOT) {
break
}
if err != nil {
t.Error(err)
break
}

readRec--
testRec := fmt.Sprintf("%05d ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789X", readRec)
if readRec > 100 {
testRec += "Y"
}
buffer := make([]byte, length)

for l := length - 1; l >= 0; l-- {
var data byte
data, err = ctx.ReadFrame()
if err != nil {
if !errors.Is(err, TapeEOR) {
t.Error(err)
}
break
}
buffer[l] = data
}

err = ctx.FinishRecord()
if err != nil {
t.Error(err)
}

sz := len(testRec) - length
if string(buffer) != testRec[sz:] {
t.Error(fmtStr + " Expected: " + testRec[sz:])
t.Error(fmtStr + " Read failed: " + string(buffer))
}

length--
if length < 0 {
length = 79
if readRec > 100 {
length++
}
}
}

if readRec != 0 {
t.Errorf(fmtStr+" Got %d records, expected: %d", rec-readRec, rec)
}
}

// Read part of record, make sure unread is skipped.
func TestShortReadE11(t *testing.T) {
testShortRead("E11", TapeFmtE11, t)
}

// Read part of record, make sure unread is skipped.
func TestShortReadTAP(t *testing.T) {
testShortRead("TAP", TapeFmtTap, t)
}

// Read part of record, make sure unread is skipped.
func TestShortReadP7B(t *testing.T) {
testShortRead("P7B", TapeFmtP7B, t)
}

// Test Odd record has pad character.
func TestOddTap(t *testing.T) {
ctx = NewTapeContext()
f, err := os.CreateTemp("", "tapeFile")
if err != nil {
return
}
fileTemp = f.Name()
f.Close()
defer os.Remove(fileTemp)

err = ctx.Attach(fileTemp, TapeFmtTap, true, false)
if err != nil {
t.Error(err)
}

if !ctx.TapeAtLoadPt() {
t.Error("Tap format not at load point")
}
if !ctx.TapeRing() {
t.Error("Tap is write protect")
}

err = ctx.WriteStart()
if err != nil {
t.Error(err)
}

testRec := "ABCDE"
for _, data := range testRec {
err = ctx.WriteFrame(byte(data))
if err != nil {
t.Error(err)
break
}
}

err = ctx.FinishRecord()
if err != nil {
t.Error(err)
}
ctx.Detach()

buffer, _ := os.ReadFile(fileTemp)

match := []byte{5, 0, 0, 0, 'A', 'B', 'C', 'D', 'E', 0, 5, 0, 0, 0}
if len(buffer) != len(match) {
t.Errorf("Tap odd record not correct length: %d should be: %d", len(buffer), len(match))
}
for i, m := range match {
if i < len(buffer) && buffer[i] != m {
t.Errorf("Tap record mismatch: %d %02x != %02x", i, buffer[i], match[i])
}
}
}

0 comments on commit fcdbb85

Please sign in to comment.