TR-DOS flat-file disk image: C++98/STL 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.

Usage

Runtime library

All parsing code for C++98/STL generated by Kaitai Struct depends on the C++/STL runtime library. You have to install it before you can parse data.

For C++, the easiest way is to clone the runtime library sources and build them along with your project.

Code

Using Kaitai Struct in C++/STL usually consists of 3 steps.

  1. We need to create an STL input stream (std::istream). One can open local file for that, or use existing std::string or char* buffer.
    #include <fstream>
    
    std::ifstream is("path/to/local/file.trd", std::ifstream::binary);
    
    #include <sstream>
    
    std::istringstream is(str);
    
    #include <sstream>
    
    const char buf[] = { ... };
    std::string str(buf, sizeof buf);
    std::istringstream is(str);
    
  2. We need to wrap our input stream into Kaitai stream:
    #include "kaitai/kaitaistream.h"
    
    kaitai::kstream ks(&is);
    
  3. And finally, we can invoke the parsing:
    tr_dos_image_t data(&ks);
    

After that, one can get various attributes from the structure by invoking getter methods like:

data.files() // => get files

C++98/STL source code to parse TR-DOS flat-file disk image

tr_dos_image.h

#ifndef TR_DOS_IMAGE_H_
#define TR_DOS_IMAGE_H_

// This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

#include "kaitai/kaitaistruct.h"
#include <stdint.h>
#include <vector>

#if KAITAI_STRUCT_VERSION < 9000L
#error "Incompatible Kaitai Struct C++/STL API: version 0.9 or later is required"
#endif

/**
 * .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.
 */

class tr_dos_image_t : public kaitai::kstruct {

public:
    class volume_info_t;
    class position_and_length_code_t;
    class filename_t;
    class position_and_length_print_t;
    class position_and_length_generic_t;
    class position_and_length_basic_t;
    class file_t;

    enum disk_type_t {
        DISK_TYPE_TYPE_80_TRACKS_DOUBLE_SIDE = 22,
        DISK_TYPE_TYPE_40_TRACKS_DOUBLE_SIDE = 23,
        DISK_TYPE_TYPE_80_TRACKS_SINGLE_SIDE = 24,
        DISK_TYPE_TYPE_40_TRACKS_SINGLE_SIDE = 25
    };

    tr_dos_image_t(kaitai::kstream* p__io, kaitai::kstruct* p__parent = 0, tr_dos_image_t* p__root = 0);

private:
    void _read();
    void _clean_up();

public:
    ~tr_dos_image_t();

    class volume_info_t : public kaitai::kstruct {

    public:

        volume_info_t(kaitai::kstream* p__io, tr_dos_image_t* p__parent = 0, tr_dos_image_t* p__root = 0);

    private:
        void _read();
        void _clean_up();

    public:
        ~volume_info_t();

    private:
        bool f_num_tracks;
        int8_t m_num_tracks;

    public:
        int8_t num_tracks();

    private:
        bool f_num_sides;
        int8_t m_num_sides;

    public:
        int8_t num_sides();

    private:
        std::string m_catalog_end;
        std::string m_unused;
        uint8_t m_first_free_sector_sector;
        uint8_t m_first_free_sector_track;
        disk_type_t m_disk_type;
        uint8_t m_num_files;
        uint16_t m_num_free_sectors;
        std::string m_tr_dos_id;
        std::string m_unused_2;
        std::string m_password;
        std::string m_unused_3;
        uint8_t m_num_deleted_files;
        std::string m_label;
        std::string m_unused_4;
        tr_dos_image_t* m__root;
        tr_dos_image_t* m__parent;

    public:
        std::string catalog_end() const { return m_catalog_end; }
        std::string unused() const { return m_unused; }
        uint8_t first_free_sector_sector() const { return m_first_free_sector_sector; }

        /**
         * 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
         */
        uint8_t first_free_sector_track() const { return m_first_free_sector_track; }
        disk_type_t disk_type() const { return m_disk_type; }

        /**
         * Number of non-deleted files. Directory can have more than
         * number_of_files entries due to deleted files
         */
        uint8_t num_files() const { return m_num_files; }
        uint16_t num_free_sectors() const { return m_num_free_sectors; }
        std::string tr_dos_id() const { return m_tr_dos_id; }
        std::string unused_2() const { return m_unused_2; }
        std::string password() const { return m_password; }
        std::string unused_3() const { return m_unused_3; }
        uint8_t num_deleted_files() const { return m_num_deleted_files; }
        std::string label() const { return m_label; }
        std::string unused_4() const { return m_unused_4; }
        tr_dos_image_t* _root() const { return m__root; }
        tr_dos_image_t* _parent() const { return m__parent; }
    };

    class position_and_length_code_t : public kaitai::kstruct {

    public:

        position_and_length_code_t(kaitai::kstream* p__io, tr_dos_image_t::file_t* p__parent = 0, tr_dos_image_t* p__root = 0);

    private:
        void _read();
        void _clean_up();

    public:
        ~position_and_length_code_t();

    private:
        uint16_t m_start_address;
        uint16_t m_length;
        tr_dos_image_t* m__root;
        tr_dos_image_t::file_t* m__parent;

    public:

        /**
         * Default memory address to load this byte array into
         */
        uint16_t start_address() const { return m_start_address; }
        uint16_t length() const { return m_length; }
        tr_dos_image_t* _root() const { return m__root; }
        tr_dos_image_t::file_t* _parent() const { return m__parent; }
    };

    class filename_t : public kaitai::kstruct {

    public:

        filename_t(kaitai::kstream* p__io, tr_dos_image_t::file_t* p__parent = 0, tr_dos_image_t* p__root = 0);

    private:
        void _read();
        void _clean_up();

    public:
        ~filename_t();

    private:
        bool f_first_byte;
        uint8_t m_first_byte;

    public:
        uint8_t first_byte();

    private:
        std::string m_name;
        tr_dos_image_t* m__root;
        tr_dos_image_t::file_t* m__parent;

    public:
        std::string name() const { return m_name; }
        tr_dos_image_t* _root() const { return m__root; }
        tr_dos_image_t::file_t* _parent() const { return m__parent; }
    };

    class position_and_length_print_t : public kaitai::kstruct {

    public:

        position_and_length_print_t(kaitai::kstream* p__io, tr_dos_image_t::file_t* p__parent = 0, tr_dos_image_t* p__root = 0);

    private:
        void _read();
        void _clean_up();

    public:
        ~position_and_length_print_t();

    private:
        uint8_t m_extent_no;
        uint8_t m_reserved;
        uint16_t m_length;
        tr_dos_image_t* m__root;
        tr_dos_image_t::file_t* m__parent;

    public:
        uint8_t extent_no() const { return m_extent_no; }
        uint8_t reserved() const { return m_reserved; }
        uint16_t length() const { return m_length; }
        tr_dos_image_t* _root() const { return m__root; }
        tr_dos_image_t::file_t* _parent() const { return m__parent; }
    };

    class position_and_length_generic_t : public kaitai::kstruct {

    public:

        position_and_length_generic_t(kaitai::kstream* p__io, tr_dos_image_t::file_t* p__parent = 0, tr_dos_image_t* p__root = 0);

    private:
        void _read();
        void _clean_up();

    public:
        ~position_and_length_generic_t();

    private:
        uint16_t m_reserved;
        uint16_t m_length;
        tr_dos_image_t* m__root;
        tr_dos_image_t::file_t* m__parent;

    public:
        uint16_t reserved() const { return m_reserved; }
        uint16_t length() const { return m_length; }
        tr_dos_image_t* _root() const { return m__root; }
        tr_dos_image_t::file_t* _parent() const { return m__parent; }
    };

    class position_and_length_basic_t : public kaitai::kstruct {

    public:

        position_and_length_basic_t(kaitai::kstream* p__io, tr_dos_image_t::file_t* p__parent = 0, tr_dos_image_t* p__root = 0);

    private:
        void _read();
        void _clean_up();

    public:
        ~position_and_length_basic_t();

    private:
        uint16_t m_program_and_data_length;
        uint16_t m_program_length;
        tr_dos_image_t* m__root;
        tr_dos_image_t::file_t* m__parent;

    public:
        uint16_t program_and_data_length() const { return m_program_and_data_length; }
        uint16_t program_length() const { return m_program_length; }
        tr_dos_image_t* _root() const { return m__root; }
        tr_dos_image_t::file_t* _parent() const { return m__parent; }
    };

    class file_t : public kaitai::kstruct {

    public:

        file_t(kaitai::kstream* p__io, tr_dos_image_t* p__parent = 0, tr_dos_image_t* p__root = 0);

    private:
        void _read();
        void _clean_up();

    public:
        ~file_t();

    private:
        bool f_is_deleted;
        bool m_is_deleted;

    public:
        bool is_deleted();

    private:
        bool f_is_terminator;
        bool m_is_terminator;

    public:
        bool is_terminator();

    private:
        bool f_contents;
        std::string m_contents;

    public:
        std::string contents();

    private:
        filename_t* m_name;
        uint8_t m_extension;
        kaitai::kstruct* m_position_and_length;
        uint8_t m_length_sectors;
        uint8_t m_starting_sector;
        uint8_t m_starting_track;
        tr_dos_image_t* m__root;
        tr_dos_image_t* m__parent;
        std::string m__raw_name;
        kaitai::kstream* m__io__raw_name;

    public:
        filename_t* name() const { return m_name; }
        uint8_t extension() const { return m_extension; }
        kaitai::kstruct* position_and_length() const { return m_position_and_length; }
        uint8_t length_sectors() const { return m_length_sectors; }
        uint8_t starting_sector() const { return m_starting_sector; }
        uint8_t starting_track() const { return m_starting_track; }
        tr_dos_image_t* _root() const { return m__root; }
        tr_dos_image_t* _parent() const { return m__parent; }
        std::string _raw_name() const { return m__raw_name; }
        kaitai::kstream* _io__raw_name() const { return m__io__raw_name; }
    };

private:
    bool f_volume_info;
    volume_info_t* m_volume_info;

public:
    volume_info_t* volume_info();

private:
    std::vector<file_t*>* m_files;
    tr_dos_image_t* m__root;
    kaitai::kstruct* m__parent;

public:
    std::vector<file_t*>* files() const { return m_files; }
    tr_dos_image_t* _root() const { return m__root; }
    kaitai::kstruct* _parent() const { return m__parent; }
};

#endif  // TR_DOS_IMAGE_H_

tr_dos_image.cpp

// This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

#include "tr_dos_image.h"
#include "kaitai/exceptions.h"

tr_dos_image_t::tr_dos_image_t(kaitai::kstream* p__io, kaitai::kstruct* p__parent, tr_dos_image_t* p__root) : kaitai::kstruct(p__io) {
    m__parent = p__parent;
    m__root = this;
    m_files = 0;
    m_volume_info = 0;
    f_volume_info = false;

    try {
        _read();
    } catch(...) {
        _clean_up();
        throw;
    }
}

void tr_dos_image_t::_read() {
    m_files = new std::vector<file_t*>();
    {
        int i = 0;
        file_t* _;
        do {
            _ = new file_t(m__io, this, m__root);
            m_files->push_back(_);
            i++;
        } while (!(_->is_terminator()));
    }
}

tr_dos_image_t::~tr_dos_image_t() {
    _clean_up();
}

void tr_dos_image_t::_clean_up() {
    if (m_files) {
        for (std::vector<file_t*>::iterator it = m_files->begin(); it != m_files->end(); ++it) {
            delete *it;
        }
        delete m_files; m_files = 0;
    }
    if (f_volume_info) {
        if (m_volume_info) {
            delete m_volume_info; m_volume_info = 0;
        }
    }
}

tr_dos_image_t::volume_info_t::volume_info_t(kaitai::kstream* p__io, tr_dos_image_t* p__parent, tr_dos_image_t* p__root) : kaitai::kstruct(p__io) {
    m__parent = p__parent;
    m__root = p__root;
    f_num_tracks = false;
    f_num_sides = false;

    try {
        _read();
    } catch(...) {
        _clean_up();
        throw;
    }
}

void tr_dos_image_t::volume_info_t::_read() {
    m_catalog_end = m__io->read_bytes(1);
    if (!(catalog_end() == std::string("\x00", 1))) {
        throw kaitai::validation_not_equal_error<std::string>(std::string("\x00", 1), catalog_end(), _io(), std::string("/types/volume_info/seq/0"));
    }
    m_unused = m__io->read_bytes(224);
    m_first_free_sector_sector = m__io->read_u1();
    m_first_free_sector_track = m__io->read_u1();
    m_disk_type = static_cast<tr_dos_image_t::disk_type_t>(m__io->read_u1());
    m_num_files = m__io->read_u1();
    m_num_free_sectors = m__io->read_u2le();
    m_tr_dos_id = m__io->read_bytes(1);
    if (!(tr_dos_id() == std::string("\x10", 1))) {
        throw kaitai::validation_not_equal_error<std::string>(std::string("\x10", 1), tr_dos_id(), _io(), std::string("/types/volume_info/seq/7"));
    }
    m_unused_2 = m__io->read_bytes(2);
    m_password = m__io->read_bytes(9);
    m_unused_3 = m__io->read_bytes(1);
    m_num_deleted_files = m__io->read_u1();
    m_label = m__io->read_bytes(8);
    m_unused_4 = m__io->read_bytes(3);
}

tr_dos_image_t::volume_info_t::~volume_info_t() {
    _clean_up();
}

void tr_dos_image_t::volume_info_t::_clean_up() {
}

int8_t tr_dos_image_t::volume_info_t::num_tracks() {
    if (f_num_tracks)
        return m_num_tracks;
    m_num_tracks = (((disk_type() & 1) != 0) ? (40) : (80));
    f_num_tracks = true;
    return m_num_tracks;
}

int8_t tr_dos_image_t::volume_info_t::num_sides() {
    if (f_num_sides)
        return m_num_sides;
    m_num_sides = (((disk_type() & 8) != 0) ? (1) : (2));
    f_num_sides = true;
    return m_num_sides;
}

tr_dos_image_t::position_and_length_code_t::position_and_length_code_t(kaitai::kstream* p__io, tr_dos_image_t::file_t* p__parent, tr_dos_image_t* p__root) : kaitai::kstruct(p__io) {
    m__parent = p__parent;
    m__root = p__root;

    try {
        _read();
    } catch(...) {
        _clean_up();
        throw;
    }
}

void tr_dos_image_t::position_and_length_code_t::_read() {
    m_start_address = m__io->read_u2le();
    m_length = m__io->read_u2le();
}

tr_dos_image_t::position_and_length_code_t::~position_and_length_code_t() {
    _clean_up();
}

void tr_dos_image_t::position_and_length_code_t::_clean_up() {
}

tr_dos_image_t::filename_t::filename_t(kaitai::kstream* p__io, tr_dos_image_t::file_t* p__parent, tr_dos_image_t* p__root) : kaitai::kstruct(p__io) {
    m__parent = p__parent;
    m__root = p__root;
    f_first_byte = false;

    try {
        _read();
    } catch(...) {
        _clean_up();
        throw;
    }
}

void tr_dos_image_t::filename_t::_read() {
    m_name = m__io->read_bytes(8);
}

tr_dos_image_t::filename_t::~filename_t() {
    _clean_up();
}

void tr_dos_image_t::filename_t::_clean_up() {
    if (f_first_byte) {
    }
}

uint8_t tr_dos_image_t::filename_t::first_byte() {
    if (f_first_byte)
        return m_first_byte;
    std::streampos _pos = m__io->pos();
    m__io->seek(0);
    m_first_byte = m__io->read_u1();
    m__io->seek(_pos);
    f_first_byte = true;
    return m_first_byte;
}

tr_dos_image_t::position_and_length_print_t::position_and_length_print_t(kaitai::kstream* p__io, tr_dos_image_t::file_t* p__parent, tr_dos_image_t* p__root) : kaitai::kstruct(p__io) {
    m__parent = p__parent;
    m__root = p__root;

    try {
        _read();
    } catch(...) {
        _clean_up();
        throw;
    }
}

void tr_dos_image_t::position_and_length_print_t::_read() {
    m_extent_no = m__io->read_u1();
    m_reserved = m__io->read_u1();
    m_length = m__io->read_u2le();
}

tr_dos_image_t::position_and_length_print_t::~position_and_length_print_t() {
    _clean_up();
}

void tr_dos_image_t::position_and_length_print_t::_clean_up() {
}

tr_dos_image_t::position_and_length_generic_t::position_and_length_generic_t(kaitai::kstream* p__io, tr_dos_image_t::file_t* p__parent, tr_dos_image_t* p__root) : kaitai::kstruct(p__io) {
    m__parent = p__parent;
    m__root = p__root;

    try {
        _read();
    } catch(...) {
        _clean_up();
        throw;
    }
}

void tr_dos_image_t::position_and_length_generic_t::_read() {
    m_reserved = m__io->read_u2le();
    m_length = m__io->read_u2le();
}

tr_dos_image_t::position_and_length_generic_t::~position_and_length_generic_t() {
    _clean_up();
}

void tr_dos_image_t::position_and_length_generic_t::_clean_up() {
}

tr_dos_image_t::position_and_length_basic_t::position_and_length_basic_t(kaitai::kstream* p__io, tr_dos_image_t::file_t* p__parent, tr_dos_image_t* p__root) : kaitai::kstruct(p__io) {
    m__parent = p__parent;
    m__root = p__root;

    try {
        _read();
    } catch(...) {
        _clean_up();
        throw;
    }
}

void tr_dos_image_t::position_and_length_basic_t::_read() {
    m_program_and_data_length = m__io->read_u2le();
    m_program_length = m__io->read_u2le();
}

tr_dos_image_t::position_and_length_basic_t::~position_and_length_basic_t() {
    _clean_up();
}

void tr_dos_image_t::position_and_length_basic_t::_clean_up() {
}

tr_dos_image_t::file_t::file_t(kaitai::kstream* p__io, tr_dos_image_t* p__parent, tr_dos_image_t* p__root) : kaitai::kstruct(p__io) {
    m__parent = p__parent;
    m__root = p__root;
    m_name = 0;
    m__io__raw_name = 0;
    f_is_deleted = false;
    f_is_terminator = false;
    f_contents = false;

    try {
        _read();
    } catch(...) {
        _clean_up();
        throw;
    }
}

void tr_dos_image_t::file_t::_read() {
    m__raw_name = m__io->read_bytes(8);
    m__io__raw_name = new kaitai::kstream(m__raw_name);
    m_name = new filename_t(m__io__raw_name, this, m__root);
    m_extension = m__io->read_u1();
    switch (extension()) {
    case 66: {
        m_position_and_length = new position_and_length_basic_t(m__io, this, m__root);
        break;
    }
    case 67: {
        m_position_and_length = new position_and_length_code_t(m__io, this, m__root);
        break;
    }
    case 35: {
        m_position_and_length = new position_and_length_print_t(m__io, this, m__root);
        break;
    }
    default: {
        m_position_and_length = new position_and_length_generic_t(m__io, this, m__root);
        break;
    }
    }
    m_length_sectors = m__io->read_u1();
    m_starting_sector = m__io->read_u1();
    m_starting_track = m__io->read_u1();
}

tr_dos_image_t::file_t::~file_t() {
    _clean_up();
}

void tr_dos_image_t::file_t::_clean_up() {
    if (m__io__raw_name) {
        delete m__io__raw_name; m__io__raw_name = 0;
    }
    if (m_name) {
        delete m_name; m_name = 0;
    }
    if (m_position_and_length) {
        delete m_position_and_length; m_position_and_length = 0;
    }
    if (f_contents) {
    }
}

bool tr_dos_image_t::file_t::is_deleted() {
    if (f_is_deleted)
        return m_is_deleted;
    m_is_deleted = name()->first_byte() == 1;
    f_is_deleted = true;
    return m_is_deleted;
}

bool tr_dos_image_t::file_t::is_terminator() {
    if (f_is_terminator)
        return m_is_terminator;
    m_is_terminator = name()->first_byte() == 0;
    f_is_terminator = true;
    return m_is_terminator;
}

std::string tr_dos_image_t::file_t::contents() {
    if (f_contents)
        return m_contents;
    std::streampos _pos = m__io->pos();
    m__io->seek((((starting_track() * 256) * 16) + (starting_sector() * 256)));
    m_contents = m__io->read_bytes((length_sectors() * 256));
    m__io->seek(_pos);
    f_contents = true;
    return m_contents;
}

tr_dos_image_t::volume_info_t* tr_dos_image_t::volume_info() {
    if (f_volume_info)
        return m_volume_info;
    std::streampos _pos = m__io->pos();
    m__io->seek(2048);
    m_volume_info = new volume_info_t(m__io, this, m__root);
    m__io->seek(_pos);
    f_volume_info = true;
    return m_volume_info;
}