TR-DOS flat-file disk image: Go parsing library

.trd file is a raw dump of TR-DOS (ZX-Spectrum) floppy. .trd files are headerless and contain consequent "logical tracks", each logical track consists of 16 256-byte sectors.

Logical tracks are defined the same way as used by TR-DOS: for single-side floppies it's just a physical track number, for two-side floppies sides are interleaved, i.e. logical_track_num = (physical_track_num << 1) | side

So, this format definition is more for TR-DOS filesystem than for .trd files, which are formatless.

Strings (file names, disk label, disk password) are padded with spaces and use ZX Spectrum character set, including UDGs, block drawing chars and Basic tokens. ASCII range is mostly standard ASCII, with few characters (^, `, DEL) replaced with (up arrow, pound, copyright symbol).

.trd file can be smaller than actual floppy disk, if last logical tracks are empty (contain no file data) they can be omitted.

File extension

trd

KS implementation details

License: CC0-1.0

References

This page hosts a formal specification of TR-DOS flat-file disk image using Kaitai Struct. This specification can be automatically translated into a variety of programming languages to get a parsing library.

Go source code to parse TR-DOS flat-file disk image

tr_dos_image.go

// Code generated by kaitai-struct-compiler from a .ksy source file. DO NOT EDIT.

import (
	"github.com/kaitai-io/kaitai_struct_go_runtime/kaitai"
	"io"
	"bytes"
)


/**
 * .trd file is a raw dump of TR-DOS (ZX-Spectrum) floppy. .trd files are
 * headerless and contain consequent "logical tracks", each logical track
 * consists of 16 256-byte sectors.
 * 
 * Logical tracks are defined the same way as used by TR-DOS: for single-side
 * floppies it's just a physical track number, for two-side floppies sides are
 * interleaved, i.e. logical_track_num = (physical_track_num << 1) | side
 * 
 * So, this format definition is more for TR-DOS filesystem than for .trd files,
 * which are formatless.
 * 
 * Strings (file names, disk label, disk password) are padded with spaces and use
 * ZX Spectrum character set, including UDGs, block drawing chars and Basic
 * tokens. ASCII range is mostly standard ASCII, with few characters (^, `, DEL)
 * replaced with (up arrow, pound, copyright symbol).
 * 
 * .trd file can be smaller than actual floppy disk, if last logical tracks are
 * empty (contain no file data) they can be omitted.
 */

type TrDosImage_DiskType int
const (
	TrDosImage_DiskType__Type80TracksDoubleSide TrDosImage_DiskType = 22
	TrDosImage_DiskType__Type40TracksDoubleSide TrDosImage_DiskType = 23
	TrDosImage_DiskType__Type80TracksSingleSide TrDosImage_DiskType = 24
	TrDosImage_DiskType__Type40TracksSingleSide TrDosImage_DiskType = 25
)
type TrDosImage struct {
	Files []*TrDosImage_File
	_io *kaitai.Stream
	_root *TrDosImage
	_parent interface{}
	_f_volumeInfo bool
	volumeInfo *TrDosImage_VolumeInfo
}
func NewTrDosImage() *TrDosImage {
	return &TrDosImage{
	}
}

func (this *TrDosImage) Read(io *kaitai.Stream, parent interface{}, root *TrDosImage) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	for i := 1;; i++ {
		tmp1 := NewTrDosImage_File()
		err = tmp1.Read(this._io, this, this._root)
		if err != nil {
			return err
		}
		_it := tmp1
		this.Files = append(this.Files, _it)
		tmp2, err := _it.IsTerminator()
		if err != nil {
			return err
		}
		if tmp2 {
			break
		}
	}
	return err
}
func (this *TrDosImage) VolumeInfo() (v *TrDosImage_VolumeInfo, err error) {
	if (this._f_volumeInfo) {
		return this.volumeInfo, nil
	}
	_pos, err := this._io.Pos()
	if err != nil {
		return nil, err
	}
	_, err = this._io.Seek(int64(2048), io.SeekStart)
	if err != nil {
		return nil, err
	}
	tmp3 := NewTrDosImage_VolumeInfo()
	err = tmp3.Read(this._io, this, this._root)
	if err != nil {
		return nil, err
	}
	this.volumeInfo = tmp3
	_, err = this._io.Seek(_pos, io.SeekStart)
	if err != nil {
		return nil, err
	}
	this._f_volumeInfo = true
	this._f_volumeInfo = true
	return this.volumeInfo, nil
}
type TrDosImage_VolumeInfo struct {
	CatalogEnd []byte
	Unused []byte
	FirstFreeSectorSector uint8
	FirstFreeSectorTrack uint8
	DiskType TrDosImage_DiskType
	NumFiles uint8
	NumFreeSectors uint16
	TrDosId []byte
	Unused2 []byte
	Password []byte
	Unused3 []byte
	NumDeletedFiles uint8
	Label []byte
	Unused4 []byte
	_io *kaitai.Stream
	_root *TrDosImage
	_parent *TrDosImage
	_f_numTracks bool
	numTracks int8
	_f_numSides bool
	numSides int8
}
func NewTrDosImage_VolumeInfo() *TrDosImage_VolumeInfo {
	return &TrDosImage_VolumeInfo{
	}
}

func (this *TrDosImage_VolumeInfo) Read(io *kaitai.Stream, parent *TrDosImage, root *TrDosImage) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	tmp4, err := this._io.ReadBytes(int(1))
	if err != nil {
		return err
	}
	tmp4 = tmp4
	this.CatalogEnd = tmp4
	if !(bytes.Equal(this.CatalogEnd, []uint8{0})) {
		return kaitai.NewValidationNotEqualError([]uint8{0}, this.CatalogEnd, this._io, "/types/volume_info/seq/0")
	}
	tmp5, err := this._io.ReadBytes(int(224))
	if err != nil {
		return err
	}
	tmp5 = tmp5
	this.Unused = tmp5
	tmp6, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.FirstFreeSectorSector = tmp6
	tmp7, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.FirstFreeSectorTrack = tmp7
	tmp8, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.DiskType = TrDosImage_DiskType(tmp8)
	tmp9, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.NumFiles = tmp9
	tmp10, err := this._io.ReadU2le()
	if err != nil {
		return err
	}
	this.NumFreeSectors = uint16(tmp10)
	tmp11, err := this._io.ReadBytes(int(1))
	if err != nil {
		return err
	}
	tmp11 = tmp11
	this.TrDosId = tmp11
	if !(bytes.Equal(this.TrDosId, []uint8{16})) {
		return kaitai.NewValidationNotEqualError([]uint8{16}, this.TrDosId, this._io, "/types/volume_info/seq/7")
	}
	tmp12, err := this._io.ReadBytes(int(2))
	if err != nil {
		return err
	}
	tmp12 = tmp12
	this.Unused2 = tmp12
	tmp13, err := this._io.ReadBytes(int(9))
	if err != nil {
		return err
	}
	tmp13 = tmp13
	this.Password = tmp13
	tmp14, err := this._io.ReadBytes(int(1))
	if err != nil {
		return err
	}
	tmp14 = tmp14
	this.Unused3 = tmp14
	tmp15, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.NumDeletedFiles = tmp15
	tmp16, err := this._io.ReadBytes(int(8))
	if err != nil {
		return err
	}
	tmp16 = tmp16
	this.Label = tmp16
	tmp17, err := this._io.ReadBytes(int(3))
	if err != nil {
		return err
	}
	tmp17 = tmp17
	this.Unused4 = tmp17
	return err
}
func (this *TrDosImage_VolumeInfo) NumTracks() (v int8, err error) {
	if (this._f_numTracks) {
		return this.numTracks, nil
	}
	var tmp18 int8;
	if ((this.DiskType & 1) != 0) {
		tmp18 = 40
	} else {
		tmp18 = 80
	}
	this.numTracks = int8(tmp18)
	this._f_numTracks = true
	return this.numTracks, nil
}
func (this *TrDosImage_VolumeInfo) NumSides() (v int8, err error) {
	if (this._f_numSides) {
		return this.numSides, nil
	}
	var tmp19 int8;
	if ((this.DiskType & 8) != 0) {
		tmp19 = 1
	} else {
		tmp19 = 2
	}
	this.numSides = int8(tmp19)
	this._f_numSides = true
	return this.numSides, nil
}

/**
 * track number is logical, for double-sided disks it's
 * (physical_track << 1) | side, the same way that tracks are stored
 * sequentially in .trd file
 */

/**
 * Number of non-deleted files. Directory can have more than
 * number_of_files entries due to deleted files
 */
type TrDosImage_PositionAndLengthCode struct {
	StartAddress uint16
	Length uint16
	_io *kaitai.Stream
	_root *TrDosImage
	_parent *TrDosImage_File
}
func NewTrDosImage_PositionAndLengthCode() *TrDosImage_PositionAndLengthCode {
	return &TrDosImage_PositionAndLengthCode{
	}
}

func (this *TrDosImage_PositionAndLengthCode) Read(io *kaitai.Stream, parent *TrDosImage_File, root *TrDosImage) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	tmp20, err := this._io.ReadU2le()
	if err != nil {
		return err
	}
	this.StartAddress = uint16(tmp20)
	tmp21, err := this._io.ReadU2le()
	if err != nil {
		return err
	}
	this.Length = uint16(tmp21)
	return err
}

/**
 * Default memory address to load this byte array into
 */
type TrDosImage_Filename struct {
	Name []byte
	_io *kaitai.Stream
	_root *TrDosImage
	_parent *TrDosImage_File
	_f_firstByte bool
	firstByte uint8
}
func NewTrDosImage_Filename() *TrDosImage_Filename {
	return &TrDosImage_Filename{
	}
}

func (this *TrDosImage_Filename) Read(io *kaitai.Stream, parent *TrDosImage_File, root *TrDosImage) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	tmp22, err := this._io.ReadBytes(int(8))
	if err != nil {
		return err
	}
	tmp22 = tmp22
	this.Name = tmp22
	return err
}
func (this *TrDosImage_Filename) FirstByte() (v uint8, err error) {
	if (this._f_firstByte) {
		return this.firstByte, nil
	}
	_pos, err := this._io.Pos()
	if err != nil {
		return 0, err
	}
	_, err = this._io.Seek(int64(0), io.SeekStart)
	if err != nil {
		return 0, err
	}
	tmp23, err := this._io.ReadU1()
	if err != nil {
		return 0, err
	}
	this.firstByte = tmp23
	_, err = this._io.Seek(_pos, io.SeekStart)
	if err != nil {
		return 0, err
	}
	this._f_firstByte = true
	this._f_firstByte = true
	return this.firstByte, nil
}
type TrDosImage_PositionAndLengthPrint struct {
	ExtentNo uint8
	Reserved uint8
	Length uint16
	_io *kaitai.Stream
	_root *TrDosImage
	_parent *TrDosImage_File
}
func NewTrDosImage_PositionAndLengthPrint() *TrDosImage_PositionAndLengthPrint {
	return &TrDosImage_PositionAndLengthPrint{
	}
}

func (this *TrDosImage_PositionAndLengthPrint) Read(io *kaitai.Stream, parent *TrDosImage_File, root *TrDosImage) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	tmp24, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.ExtentNo = tmp24
	tmp25, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.Reserved = tmp25
	tmp26, err := this._io.ReadU2le()
	if err != nil {
		return err
	}
	this.Length = uint16(tmp26)
	return err
}
type TrDosImage_PositionAndLengthGeneric struct {
	Reserved uint16
	Length uint16
	_io *kaitai.Stream
	_root *TrDosImage
	_parent *TrDosImage_File
}
func NewTrDosImage_PositionAndLengthGeneric() *TrDosImage_PositionAndLengthGeneric {
	return &TrDosImage_PositionAndLengthGeneric{
	}
}

func (this *TrDosImage_PositionAndLengthGeneric) Read(io *kaitai.Stream, parent *TrDosImage_File, root *TrDosImage) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	tmp27, err := this._io.ReadU2le()
	if err != nil {
		return err
	}
	this.Reserved = uint16(tmp27)
	tmp28, err := this._io.ReadU2le()
	if err != nil {
		return err
	}
	this.Length = uint16(tmp28)
	return err
}
type TrDosImage_PositionAndLengthBasic struct {
	ProgramAndDataLength uint16
	ProgramLength uint16
	_io *kaitai.Stream
	_root *TrDosImage
	_parent *TrDosImage_File
}
func NewTrDosImage_PositionAndLengthBasic() *TrDosImage_PositionAndLengthBasic {
	return &TrDosImage_PositionAndLengthBasic{
	}
}

func (this *TrDosImage_PositionAndLengthBasic) Read(io *kaitai.Stream, parent *TrDosImage_File, root *TrDosImage) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	tmp29, err := this._io.ReadU2le()
	if err != nil {
		return err
	}
	this.ProgramAndDataLength = uint16(tmp29)
	tmp30, err := this._io.ReadU2le()
	if err != nil {
		return err
	}
	this.ProgramLength = uint16(tmp30)
	return err
}
type TrDosImage_File struct {
	Name *TrDosImage_Filename
	Extension uint8
	PositionAndLength interface{}
	LengthSectors uint8
	StartingSector uint8
	StartingTrack uint8
	_io *kaitai.Stream
	_root *TrDosImage
	_parent *TrDosImage
	_raw_Name []byte
	_f_isDeleted bool
	isDeleted bool
	_f_isTerminator bool
	isTerminator bool
	_f_contents bool
	contents []byte
}
func NewTrDosImage_File() *TrDosImage_File {
	return &TrDosImage_File{
	}
}

func (this *TrDosImage_File) Read(io *kaitai.Stream, parent *TrDosImage, root *TrDosImage) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	tmp31, err := this._io.ReadBytes(int(8))
	if err != nil {
		return err
	}
	tmp31 = tmp31
	this._raw_Name = tmp31
	_io__raw_Name := kaitai.NewStream(bytes.NewReader(this._raw_Name))
	tmp32 := NewTrDosImage_Filename()
	err = tmp32.Read(_io__raw_Name, this, this._root)
	if err != nil {
		return err
	}
	this.Name = tmp32
	tmp33, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.Extension = tmp33
	switch (this.Extension) {
	case 66:
		tmp34 := NewTrDosImage_PositionAndLengthBasic()
		err = tmp34.Read(this._io, this, this._root)
		if err != nil {
			return err
		}
		this.PositionAndLength = tmp34
	case 67:
		tmp35 := NewTrDosImage_PositionAndLengthCode()
		err = tmp35.Read(this._io, this, this._root)
		if err != nil {
			return err
		}
		this.PositionAndLength = tmp35
	case 35:
		tmp36 := NewTrDosImage_PositionAndLengthPrint()
		err = tmp36.Read(this._io, this, this._root)
		if err != nil {
			return err
		}
		this.PositionAndLength = tmp36
	default:
		tmp37 := NewTrDosImage_PositionAndLengthGeneric()
		err = tmp37.Read(this._io, this, this._root)
		if err != nil {
			return err
		}
		this.PositionAndLength = tmp37
	}
	tmp38, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.LengthSectors = tmp38
	tmp39, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.StartingSector = tmp39
	tmp40, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.StartingTrack = tmp40
	return err
}
func (this *TrDosImage_File) IsDeleted() (v bool, err error) {
	if (this._f_isDeleted) {
		return this.isDeleted, nil
	}
	tmp41, err := this.Name.FirstByte()
	if err != nil {
		return false, err
	}
	this.isDeleted = bool(tmp41 == 1)
	this._f_isDeleted = true
	return this.isDeleted, nil
}
func (this *TrDosImage_File) IsTerminator() (v bool, err error) {
	if (this._f_isTerminator) {
		return this.isTerminator, nil
	}
	tmp42, err := this.Name.FirstByte()
	if err != nil {
		return false, err
	}
	this.isTerminator = bool(tmp42 == 0)
	this._f_isTerminator = true
	return this.isTerminator, nil
}
func (this *TrDosImage_File) Contents() (v []byte, err error) {
	if (this._f_contents) {
		return this.contents, nil
	}
	_pos, err := this._io.Pos()
	if err != nil {
		return nil, err
	}
	_, err = this._io.Seek(int64((((this.StartingTrack * 256) * 16) + (this.StartingSector * 256))), io.SeekStart)
	if err != nil {
		return nil, err
	}
	tmp43, err := this._io.ReadBytes(int((this.LengthSectors * 256)))
	if err != nil {
		return nil, err
	}
	tmp43 = tmp43
	this.contents = tmp43
	_, err = this._io.Seek(_pos, io.SeekStart)
	if err != nil {
		return nil, err
	}
	this._f_contents = true
	this._f_contents = true
	return this.contents, nil
}