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
)
var values_TrDosImage_DiskType = map[TrDosImage_DiskType]struct{}{22: {}, 23: {}, 24: {}, 25: {}}
func (v TrDosImage_DiskType) isDefined() bool {
	_, ok := values_TrDosImage_DiskType[v]
	return ok
}
type TrDosImage struct {
	Files []*TrDosImage_File
	_io *kaitai.Stream
	_root *TrDosImage
	_parent kaitai.Struct
	_f_volumeInfo bool
	volumeInfo *TrDosImage_VolumeInfo
}
func NewTrDosImage() *TrDosImage {
	return &TrDosImage{
	}
}

func (this TrDosImage) IO_() *kaitai.Stream {
	return this._io
}

func (this *TrDosImage) Read(io *kaitai.Stream, parent kaitai.Struct, 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
	}
	this._f_volumeInfo = true
	_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
	}
	return this.volumeInfo, nil
}
type TrDosImage_File struct {
	Name *TrDosImage_Filename
	Extension uint8
	PositionAndLength kaitai.Struct
	LengthSectors uint8
	StartingSector uint8
	StartingTrack uint8
	_io *kaitai.Stream
	_root *TrDosImage
	_parent *TrDosImage
	_raw_Name []byte
	_f_contents bool
	contents []byte
	_f_isDeleted bool
	isDeleted bool
	_f_isTerminator bool
	isTerminator bool
}
func NewTrDosImage_File() *TrDosImage_File {
	return &TrDosImage_File{
	}
}

func (this TrDosImage_File) IO_() *kaitai.Stream {
	return this._io
}

func (this *TrDosImage_File) 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(8))
	if err != nil {
		return err
	}
	tmp4 = tmp4
	this._raw_Name = tmp4
	_io__raw_Name := kaitai.NewStream(bytes.NewReader(this._raw_Name))
	tmp5 := NewTrDosImage_Filename()
	err = tmp5.Read(_io__raw_Name, this, this._root)
	if err != nil {
		return err
	}
	this.Name = tmp5
	tmp6, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.Extension = tmp6
	switch (this.Extension) {
	case 35:
		tmp7 := NewTrDosImage_PositionAndLengthPrint()
		err = tmp7.Read(this._io, this, this._root)
		if err != nil {
			return err
		}
		this.PositionAndLength = tmp7
	case 66:
		tmp8 := NewTrDosImage_PositionAndLengthBasic()
		err = tmp8.Read(this._io, this, this._root)
		if err != nil {
			return err
		}
		this.PositionAndLength = tmp8
	case 67:
		tmp9 := NewTrDosImage_PositionAndLengthCode()
		err = tmp9.Read(this._io, this, this._root)
		if err != nil {
			return err
		}
		this.PositionAndLength = tmp9
	default:
		tmp10 := NewTrDosImage_PositionAndLengthGeneric()
		err = tmp10.Read(this._io, this, this._root)
		if err != nil {
			return err
		}
		this.PositionAndLength = tmp10
	}
	tmp11, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.LengthSectors = tmp11
	tmp12, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.StartingSector = tmp12
	tmp13, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.StartingTrack = tmp13
	return err
}
func (this *TrDosImage_File) Contents() (v []byte, err error) {
	if (this._f_contents) {
		return this.contents, nil
	}
	this._f_contents = true
	_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
	}
	tmp14, err := this._io.ReadBytes(int(this.LengthSectors * 256))
	if err != nil {
		return nil, err
	}
	tmp14 = tmp14
	this.contents = tmp14
	_, err = this._io.Seek(_pos, io.SeekStart)
	if err != nil {
		return nil, err
	}
	return this.contents, nil
}
func (this *TrDosImage_File) IsDeleted() (v bool, err error) {
	if (this._f_isDeleted) {
		return this.isDeleted, nil
	}
	this._f_isDeleted = true
	tmp15, err := this.Name.FirstByte()
	if err != nil {
		return false, err
	}
	this.isDeleted = bool(tmp15 == 1)
	return this.isDeleted, nil
}
func (this *TrDosImage_File) IsTerminator() (v bool, err error) {
	if (this._f_isTerminator) {
		return this.isTerminator, nil
	}
	this._f_isTerminator = true
	tmp16, err := this.Name.FirstByte()
	if err != nil {
		return false, err
	}
	this.isTerminator = bool(tmp16 == 0)
	return this.isTerminator, nil
}
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) IO_() *kaitai.Stream {
	return this._io
}

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

	tmp17, err := this._io.ReadBytes(int(8))
	if err != nil {
		return err
	}
	tmp17 = tmp17
	this.Name = tmp17
	return err
}
func (this *TrDosImage_Filename) FirstByte() (v uint8, err error) {
	if (this._f_firstByte) {
		return this.firstByte, nil
	}
	this._f_firstByte = true
	_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
	}
	tmp18, err := this._io.ReadU1()
	if err != nil {
		return 0, err
	}
	this.firstByte = tmp18
	_, err = this._io.Seek(_pos, io.SeekStart)
	if err != nil {
		return 0, err
	}
	return this.firstByte, nil
}
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) IO_() *kaitai.Stream {
	return this._io
}

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

	tmp19, err := this._io.ReadU2le()
	if err != nil {
		return err
	}
	this.ProgramAndDataLength = uint16(tmp19)
	tmp20, err := this._io.ReadU2le()
	if err != nil {
		return err
	}
	this.ProgramLength = uint16(tmp20)
	return err
}
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) IO_() *kaitai.Stream {
	return this._io
}

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

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

/**
 * Default memory address to load this byte array into
 */
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) IO_() *kaitai.Stream {
	return this._io
}

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

	tmp23, err := this._io.ReadU2le()
	if err != nil {
		return err
	}
	this.Reserved = uint16(tmp23)
	tmp24, err := this._io.ReadU2le()
	if err != nil {
		return err
	}
	this.Length = uint16(tmp24)
	return err
}
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) IO_() *kaitai.Stream {
	return this._io
}

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

	tmp25, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.ExtentNo = tmp25
	tmp26, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.Reserved = tmp26
	tmp27, err := this._io.ReadU2le()
	if err != nil {
		return err
	}
	this.Length = uint16(tmp27)
	return err
}
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_numSides bool
	numSides int8
	_f_numTracks bool
	numTracks int8
}
func NewTrDosImage_VolumeInfo() *TrDosImage_VolumeInfo {
	return &TrDosImage_VolumeInfo{
	}
}

func (this TrDosImage_VolumeInfo) IO_() *kaitai.Stream {
	return this._io
}

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

	tmp28, err := this._io.ReadBytes(int(1))
	if err != nil {
		return err
	}
	tmp28 = tmp28
	this.CatalogEnd = tmp28
	if !(bytes.Equal(this.CatalogEnd, []uint8{0})) {
		return kaitai.NewValidationNotEqualError([]uint8{0}, this.CatalogEnd, this._io, "/types/volume_info/seq/0")
	}
	tmp29, err := this._io.ReadBytes(int(224))
	if err != nil {
		return err
	}
	tmp29 = tmp29
	this.Unused = tmp29
	tmp30, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.FirstFreeSectorSector = tmp30
	tmp31, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.FirstFreeSectorTrack = tmp31
	tmp32, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.DiskType = TrDosImage_DiskType(tmp32)
	tmp33, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.NumFiles = tmp33
	tmp34, err := this._io.ReadU2le()
	if err != nil {
		return err
	}
	this.NumFreeSectors = uint16(tmp34)
	tmp35, err := this._io.ReadBytes(int(1))
	if err != nil {
		return err
	}
	tmp35 = tmp35
	this.TrDosId = tmp35
	if !(bytes.Equal(this.TrDosId, []uint8{16})) {
		return kaitai.NewValidationNotEqualError([]uint8{16}, this.TrDosId, this._io, "/types/volume_info/seq/7")
	}
	tmp36, err := this._io.ReadBytes(int(2))
	if err != nil {
		return err
	}
	tmp36 = tmp36
	this.Unused2 = tmp36
	tmp37, err := this._io.ReadBytes(int(9))
	if err != nil {
		return err
	}
	tmp37 = tmp37
	this.Password = tmp37
	tmp38, err := this._io.ReadBytes(int(1))
	if err != nil {
		return err
	}
	tmp38 = tmp38
	this.Unused3 = tmp38
	tmp39, err := this._io.ReadU1()
	if err != nil {
		return err
	}
	this.NumDeletedFiles = tmp39
	tmp40, err := this._io.ReadBytes(int(8))
	if err != nil {
		return err
	}
	tmp40 = tmp40
	this.Label = tmp40
	tmp41, err := this._io.ReadBytes(int(3))
	if err != nil {
		return err
	}
	tmp41 = tmp41
	this.Unused4 = tmp41
	return err
}
func (this *TrDosImage_VolumeInfo) NumSides() (v int8, err error) {
	if (this._f_numSides) {
		return this.numSides, nil
	}
	this._f_numSides = true
	var tmp42 int8;
	if (this.DiskType & 8 != 0) {
		tmp42 = 1
	} else {
		tmp42 = 2
	}
	this.numSides = int8(tmp42)
	return this.numSides, nil
}
func (this *TrDosImage_VolumeInfo) NumTracks() (v int8, err error) {
	if (this._f_numTracks) {
		return this.numTracks, nil
	}
	this._f_numTracks = true
	var tmp43 int8;
	if (this.DiskType & 1 != 0) {
		tmp43 = 40
	} else {
		tmp43 = 80
	}
	this.numTracks = int8(tmp43)
	return this.numTracks, 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
 */