Task 699: .STX File Format

Task 699: .STX File Format

1. List of Properties for .STX File Format (Pasti Atari ST Disk Image)

Based on the specifications of the Pasti .STX file format, which is a disk image format for preserving Atari ST floppy disks (including copy-protected ones), the key intrinsic properties (structures and fields) are as follows. These are derived from the file's binary layout and are essential to its structure as a disk image format emulating the Atari ST file system and floppy characteristics. All multi-byte values are little-endian unless noted.

File Descriptor (Header, 16 bytes):

  • File Identifier (4 bytes): String "RSY\0".
  • Version (2 bytes): Typically 0x03.
  • Tool (2 bytes): 0x01 for Atari imaging tool, 0xCC for Discovery Cartridge tool.
  • Reserved 1 (2 bytes): Usually 0x00.
  • Track Count (1 byte): Number of tracks (e.g., 80 or 160 for double-sided).
  • Revision (1 byte): 0x00 for old format, 0x02 for new (affects sector timing handling).
  • Reserved 2 (4 bytes): Usually 0x00 (may be 0xCCCC in early files).

Track Descriptor (Per Track, 16 bytes):

  • Record Size (4 bytes): Total size of this track record.
  • Fuzzy Count (4 bytes): Size of optional fuzzy mask in bytes.
  • Sector Count (2 bytes): Number of sectors in the track (0 for empty/unformatted).
  • Track Flags (2 bytes): Bitmask (e.g., bit 7: sync info present; bit 6: track image present; bit 0: sector descriptors present).
  • Track Length (2 bytes): Length of track in bytes (typically ~6250).
  • Track Number (1 byte): Track ID (bits 0-6: track 0-79/81; bit 7: side, 0=A, 1=B).
  • Track Type (1 byte): 0x00 for WD1772 dump, 0xCC for Discovery Cartridge dump.

Sector Descriptor (Optional, per sector if track flags bit 0 set, 16 bytes each):

  • Data Offset (4 bytes): Offset to sector data in track data record.
  • Bit Position (2 bytes): Position of sector address block in bits from track start.
  • Read Time (2 bytes): Sector read time in μs (0 for standard ~16384 μs).
  • Track (1 byte): Track number from address block.
  • Head (1 byte): Head/side number (0 or 1).
  • Number (1 byte): Sector number (typically 1-11).
  • Size (1 byte): Sector size code (0=128 bytes, 1=256, 2=512, 3=1024; actual size = 128 << size).
  • CRC (2 bytes): CRC16 of address block.
  • FDC Flags (1 byte): FDC status (bit 7: fuzzy bits; bit 5: deleted data; bit 4: record not found; bit 3: CRC error; bit 0: intra-sector bit width variation).
  • Reserved (1 byte): Usually 0x00.

Fuzzy Mask (Optional, variable size based on fuzzy count):

  • Mask Bytes (variable): Bitmask for fuzzy (non-deterministic) bits in sectors; applied to sectors with FDC flags bit 7 set.

Track Data Record (Variable):

  • Optional Track Image Header (2 or 4 bytes if track flags bit 6 set): Length (2 bytes) or sync offset (2 bytes) + length (2 bytes) if bit 7 set.
  • Track Image Content (variable): Raw track dump (WD1772 or DC type).
  • Optional Sector Images (variable): Individual sector data if not embedded in track image.

Timing Record (Optional, for revision 0x02 with intra-sector variations):

  • Timing Descriptor + Data (variable): Describes bit width variations (e.g., for Macrodos/Speedlock protections).

These properties capture the format's ability to represent low-level floppy details like protections, fuzzy bits, and timing, which are intrinsic to emulating the Atari ST's TOS/GEMDOS file system on floppies.

These are direct links to Atari ST game disk images in .STX format (Afterburner, Disks 1 and 2).

3. Ghost Blog Embedded HTML/JavaScript for Drag-and-Drop .STX File Dump

STX File Dumper
Drag and drop .STX file here

This HTML/JS allows dragging and dropping a .STX file, parses the binary, and dumps the properties (headers and fields) to the screen. It focuses on reading headers; full data skipping is simplified for brevity.

4. Python Class for .STX File Handling

import struct
import os

class STXFile:
    def __init__(self, filepath=None):
        self.filepath = filepath
        self.file_header = None
        self.tracks = []
        if filepath:
            self.read()

    def read(self):
        with open(self.filepath, 'rb') as f:
            data = f.read()
        offset = 0

        # File Descriptor
        self.file_header = {}
        self.file_header['id'] = struct.unpack_from('<4s', data, offset)[0].decode('ascii')
        offset += 4
        self.file_header['version'] = struct.unpack_from('<H', data, offset)[0]
        offset += 2
        self.file_header['tool'] = struct.unpack_from('<H', data, offset)[0]
        offset += 2
        self.file_header['reserved1'] = struct.unpack_from('<H', data, offset)[0]
        offset += 2
        self.file_header['track_count'] = struct.unpack_from('<B', data, offset)[0]
        offset += 1
        self.file_header['revision'] = struct.unpack_from('<B', data, offset)[0]
        offset += 1
        self.file_header['reserved2'] = struct.unpack_from('<I', data, offset)[0]
        offset += 4

        for _ in range(self.file_header['track_count']):
            track = {}
            track['record_size'] = struct.unpack_from('<I', data, offset)[0]
            offset += 4
            track['fuzzy_count'] = struct.unpack_from('<I', data, offset)[0]
            offset += 4
            track['sector_count'] = struct.unpack_from('<H', data, offset)[0]
            offset += 2
            track['flags'] = struct.unpack_from('<H', data, offset)[0]
            offset += 2
            track['length'] = struct.unpack_from('<H', data, offset)[0]
            offset += 2
            track['number'] = struct.unpack_from('<B', data, offset)[0]
            offset += 1
            track['type'] = struct.unpack_from('<B', data, offset)[0]
            offset += 1

            track['sectors'] = []
            if track['flags'] & 0x01:
                for __ in range(track['sector_count']):
                    sector = {}
                    sector['data_offset'] = struct.unpack_from('<I', data, offset)[0]
                    offset += 4
                    sector['bit_position'] = struct.unpack_from('<H', data, offset)[0]
                    offset += 2
                    sector['read_time'] = struct.unpack_from('<H', data, offset)[0]
                    offset += 2
                    sector['track'] = struct.unpack_from('<B', data, offset)[0]
                    offset += 1
                    sector['head'] = struct.unpack_from('<B', data, offset)[0]
                    offset += 1
                    sector['number'] = struct.unpack_from('<B', data, offset)[0]
                    offset += 1
                    sector['size'] = struct.unpack_from('<B', data, offset)[0]
                    offset += 1
                    sector['crc'] = struct.unpack_from('<H', data, offset)[0]
                    offset += 2
                    sector['fdc_flags'] = struct.unpack_from('<B', data, offset)[0]
                    offset += 1
                    sector['reserved'] = struct.unpack_from('<B', data, offset)[0]
                    offset += 1
                    track['sectors'].append(sector)

            # Skip fuzzy, track data, timing for now (can add full decoding if needed)
            # Advance by remaining record size (track['record_size'] - parsed bytes)
            parsed_track_bytes = 16 + (16 * track['sector_count'] if track['flags'] & 0x01 else 0)
            offset += track['record_size'] - parsed_track_bytes  # Simplified; adjust for full

            self.tracks.append(track)

    def print_properties(self):
        print('File Header:')
        for k, v in self.file_header.items():
            print(f'  {k}: {v}')
        for i, track in enumerate(self.tracks):
            print(f'\nTrack {i}:')
            for k, v in track.items():
                if k != 'sectors':
                    print(f'  {k}: {v}')
            for j, sector in enumerate(track.get('sectors', [])):
                print(f'    Sector {j}:')
                for sk, sv in sector.items():
                    print(f'      {sk}: {sv}')

    def write(self, new_filepath=None):
        if not new_filepath:
            new_filepath = self.filepath + '.new'
        with open(new_filepath, 'wb') as f:
            # Write File Descriptor
            f.write(struct.pack('<4s', self.file_header['id'].encode('ascii')))
            f.write(struct.pack('<H', self.file_header['version']))
            f.write(struct.pack('<H', self.file_header['tool']))
            f.write(struct.pack('<H', self.file_header['reserved1']))
            f.write(struct.pack('<B', self.file_header['track_count']))
            f.write(struct.pack('<B', self.file_header['revision']))
            f.write(struct.pack('<I', self.file_header['reserved2']))

            # Write tracks (simplified; assumes no data changes, skips full data write for brevity)
            for track in self.tracks:
                f.write(struct.pack('<I', track['record_size']))
                f.write(struct.pack('<I', track['fuzzy_count']))
                f.write(struct.pack('<H', track['sector_count']))
                f.write(struct.pack('<H', track['flags']))
                f.write(struct.pack('<H', track['length']))
                f.write(struct.pack('<B', track['number']))
                f.write(struct.pack('<B', track['type']))
                if track['flags'] & 0x01:
                    for sector in track['sectors']:
                        f.write(struct.pack('<I', sector['data_offset']))
                        f.write(struct.pack('<H', sector['bit_position']))
                        f.write(struct.pack('<H', sector['read_time']))
                        f.write(struct.pack('<B', sector['track']))
                        f.write(struct.pack('<B', sector['head']))
                        f.write(struct.pack('<B', sector['number']))
                        f.write(struct.pack('<B', sector['size']))
                        f.write(struct.pack('<H', sector['crc']))
                        f.write(struct.pack('<B', sector['fdc_flags']))
                        f.write(struct.pack('<B', sector['reserved']))
                # Add fuzzy/track data write logic if needed; omitted for brevity

# Example usage:
# stx = STXFile('example.stx')
# stx.print_properties()
# stx.write()

This class reads a .STX file, decodes headers, prints properties to console, and supports basic writing (headers only; full data write requires additional logic for fuzzy/track content).

5. Java Class for .STX File Handling

import java.io.*;
import java.nio.*;
import java.nio.file.*;

public class STXFile {
    private Path filepath;
    private ByteBuffer buffer;
    private int trackCount;
    private byte[] fileHeader = new byte[16];
    // Simplified; use maps or classes for full properties

    public STXFile(String filepath) throws IOException {
        this.filepath = Paths.get(filepath);
        byte[] data = Files.readAllBytes(this.filepath);
        buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
        read();
    }

    private void read() {
        buffer.position(0);
        buffer.get(fileHeader); // Full header read
        trackCount = Byte.toUnsignedInt(fileHeader[10]);

        System.out.println("File Header:");
        System.out.printf("  ID: %s\n", new String(fileHeader, 0, 4));
        System.out.printf("  Version: 0x%04X\n", Short.toUnsignedInt(buffer.getShort(4)));
        System.out.printf("  Tool: 0x%04X\n", Short.toUnsignedInt(buffer.getShort(6)));
        System.out.printf("  Reserved1: 0x%04X\n", Short.toUnsignedInt(buffer.getShort(8)));
        System.out.printf("  Track Count: %d\n", trackCount);
        System.out.printf("  Revision: 0x%02X\n", fileHeader[11]);
        System.out.printf("  Reserved2: 0x%08X\n", buffer.getInt(12));

        int offset = 16;
        for (int t = 0; t < trackCount; t++) {
            System.out.printf("\nTrack %d:\n", t);
            int recordSize = buffer.getInt(offset);
            System.out.printf("  Record Size: %d\n", recordSize);
            offset += 4;
            int fuzzyCount = buffer.getInt(offset);
            System.out.printf("  Fuzzy Count: %d\n", fuzzyCount);
            offset += 4;
            int sectorCount = Short.toUnsignedInt(buffer.getShort(offset));
            System.out.printf("  Sector Count: %d\n", sectorCount);
            offset += 2;
            int flags = Short.toUnsignedInt(buffer.getShort(offset));
            System.out.printf("  Flags: 0x%04X\n", flags);
            offset += 2;
            int length = Short.toUnsignedInt(buffer.getShort(offset));
            System.out.printf("  Length: %d\n", length);
            offset += 2;
            int number = Byte.toUnsignedInt(buffer.get(offset));
            System.out.printf("  Number: %d\n", number);
            offset += 1;
            int type = Byte.toUnsignedInt(buffer.get(offset));
            System.out.printf("  Type: 0x%02X\n", type);
            offset += 1;

            if ((flags & 0x01) != 0) {
                for (int s = 0; s < sectorCount; s++) {
                    System.out.printf("    Sector %d:\n", s);
                    System.out.printf("      Data Offset: %d\n", buffer.getInt(offset));
                    offset += 4;
                    System.out.printf("      Bit Position: %d\n", Short.toUnsignedInt(buffer.getShort(offset)));
                    offset += 2;
                    System.out.printf("      Read Time: %d\n", Short.toUnsignedInt(buffer.getShort(offset)));
                    offset += 2;
                    System.out.printf("      Track: %d\n", Byte.toUnsignedInt(buffer.get(offset)));
                    offset += 1;
                    System.out.printf("      Head: %d\n", Byte.toUnsignedInt(buffer.get(offset)));
                    offset += 1;
                    System.out.printf("      Number: %d\n", Byte.toUnsignedInt(buffer.get(offset)));
                    offset += 1;
                    System.out.printf("      Size: %d\n", Byte.toUnsignedInt(buffer.get(offset)));
                    offset += 1;
                    System.out.printf("      CRC: 0x%04X\n", Short.toUnsignedInt(buffer.getShort(offset)));
                    offset += 2;
                    System.out.printf("      FDC Flags: 0x%02X\n", Byte.toUnsignedInt(buffer.get(offset)));
                    offset += 1;
                    System.out.printf("      Reserved: 0x%02X\n", Byte.toUnsignedInt(buffer.get(offset)));
                    offset += 1;
                }
            }
            // Skip remaining (fuzzy/track data)
            int parsed = 16 + ((flags & 0x01) != 0 ? 16 * sectorCount : 0);
            offset += recordSize - parsed;
        }
    }

    public void write(String newFilepath) throws IOException {
        // Simplified write: copy original for now; add modifications as needed
        Files.copy(filepath, Paths.get(newFilepath));
    }

    public void printProperties() {
        // Read already prints; call read() again if needed
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     STXFile stx = new STXFile("example.stx");
    //     stx.write("example.new.stx");
    // }
}

This Java class opens a .STX file, decodes and prints properties to console, and supports basic writing (copy; extend for modifications).

6. JavaScript Class for .STX File Handling

class STXFile {
    constructor(buffer = null) {
        this.buffer = buffer;
        this.view = buffer ? new DataView(buffer) : null;
        this.fileHeader = {};
        this.tracks = [];
        if (buffer) this.read();
    }

    read() {
        let offset = 0;

        // File Descriptor
        this.fileHeader.id = String.fromCharCode(this.view.getUint8(offset++), this.view.getUint8(offset++), this.view.getUint8(offset++), this.view.getUint8(offset++));
        this.fileHeader.version = this.view.getUint16(offset, true); offset += 2;
        this.fileHeader.tool = this.view.getUint16(offset, true); offset += 2;
        this.fileHeader.reserved1 = this.view.getUint16(offset, true); offset += 2;
        this.fileHeader.trackCount = this.view.getUint8(offset++); 
        this.fileHeader.revision = this.view.getUint8(offset++);
        this.fileHeader.reserved2 = this.view.getUint32(offset, true); offset += 4;

        for (let t = 0; t < this.fileHeader.trackCount; t++) {
            let track = {};
            track.recordSize = this.view.getUint32(offset, true); offset += 4;
            track.fuzzyCount = this.view.getUint32(offset, true); offset += 4;
            track.sectorCount = this.view.getUint16(offset, true); offset += 2;
            track.flags = this.view.getUint16(offset, true); offset += 2;
            track.length = this.view.getUint16(offset, true); offset += 2;
            track.number = this.view.getUint8(offset++);
            track.type = this.view.getUint8(offset++);

            track.sectors = [];
            if (track.flags & 0x01) {
                for (let s = 0; s < track.sectorCount; s++) {
                    let sector = {};
                    sector.dataOffset = this.view.getUint32(offset, true); offset += 4;
                    sector.bitPosition = this.view.getUint16(offset, true); offset += 2;
                    sector.readTime = this.view.getUint16(offset, true); offset += 2;
                    sector.track = this.view.getUint8(offset++);
                    sector.head = this.view.getUint8(offset++);
                    sector.number = this.view.getUint8(offset++);
                    sector.size = this.view.getUint8(offset++);
                    sector.crc = this.view.getUint16(offset, true); offset += 2;
                    sector.fdcFlags = this.view.getUint8(offset++);
                    sector.reserved = this.view.getUint8(offset++);
                    track.sectors.push(sector);
                }
            }
            // Skip data
            let parsed = 16 + ((track.flags & 0x01) ? 16 * track.sectorCount : 0);
            offset += track.recordSize - parsed;

            this.tracks.push(track);
        }
    }

    printProperties() {
        console.log('File Header:');
        console.log(this.fileHeader);
        this.tracks.forEach((track, i) => {
            console.log(`\nTrack ${i}:`);
            console.log(track);
        });
    }

    write() {
        // Simplified: return new buffer with headers; extend for full
        const newBuffer = new ArrayBuffer(this.buffer.byteLength);
        const newView = new DataView(newBuffer);
        // Copy and write logic similar to read, but reverse
        // Omitted for brevity; implement struct packing
        return newBuffer;
    }
}

// Example usage (Node.js with fs):
// const fs = require('fs');
// fs.readFile('example.stx', (err, data) => {
//     const stx = new STXFile(data.buffer);
//     stx.printProperties();
// });

This JS class (for browser/Node) reads from an ArrayBuffer, decodes, prints properties to console, and stubs writing.

7. C Class for .STX File Handling

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

typedef struct {
    char id[4];
    uint16_t version;
    uint16_t tool;
    uint16_t reserved1;
    uint8_t trackCount;
    uint8_t revision;
    uint32_t reserved2;
} FileHeader;

typedef struct {
    uint32_t recordSize;
    uint32_t fuzzyCount;
    uint16_t sectorCount;
    uint16_t flags;
    uint16_t length;
    uint8_t number;
    uint8_t type;
} TrackHeader;

typedef struct {
    uint32_t dataOffset;
    uint16_t bitPosition;
    uint16_t readTime;
    uint8_t track;
    uint8_t head;
    uint8_t number;
    uint8_t size;
    uint16_t crc;
    uint8_t fdcFlags;
    uint8_t reserved;
} SectorHeader;

typedef struct {
    FileHeader fileHeader;
    TrackHeader* tracks;
    SectorHeader** sectors; // Array of pointers to sector arrays
} STXFile;

STXFile* stx_open(const char* filepath) {
    FILE* f = fopen(filepath, "rb");
    if (!f) return NULL;

    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, 0, SEEK_SET);

    uint8_t* data = malloc(size);
    fread(data, 1, size, f);
    fclose(f);

    STXFile* stx = malloc(sizeof(STXFile));
    uint8_t* ptr = data;

    memcpy(stx->fileHeader.id, ptr, 4); ptr += 4;
    stx->fileHeader.version = *(uint16_t*)ptr; ptr += 2;
    stx->fileHeader.tool = *(uint16_t*)ptr; ptr += 2;
    stx->fileHeader.reserved1 = *(uint16_t*)ptr; ptr += 2;
    stx->fileHeader.trackCount = *ptr++;
    stx->fileHeader.revision = *ptr++;
    stx->fileHeader.reserved2 = *(uint32_t*)ptr; ptr += 4;

    stx->tracks = malloc(stx->fileHeader.trackCount * sizeof(TrackHeader));
    stx->sectors = malloc(stx->fileHeader.trackCount * sizeof(SectorHeader*));

    for (uint8_t t = 0; t < stx->fileHeader.trackCount; t++) {
        stx->tracks[t].recordSize = *(uint32_t*)ptr; ptr += 4;
        stx->tracks[t].fuzzyCount = *(uint32_t*)ptr; ptr += 4;
        stx->tracks[t].sectorCount = *(uint16_t*)ptr; ptr += 2;
        stx->tracks[t].flags = *(uint16_t*)ptr; ptr += 2;
        stx->tracks[t].length = *(uint16_t*)ptr; ptr += 2;
        stx->tracks[t].number = *ptr++;
        stx->tracks[t].type = *ptr++;

        if (stx->tracks[t].flags & 0x01) {
            stx->sectors[t] = malloc(stx->tracks[t].sectorCount * sizeof(SectorHeader));
            for (uint16_t s = 0; s < stx->tracks[t].sectorCount; s++) {
                stx->sectors[t][s].dataOffset = *(uint32_t*)ptr; ptr += 4;
                stx->sectors[t][s].bitPosition = *(uint16_t*)ptr; ptr += 2;
                stx->sectors[t][s].readTime = *(uint16_t*)ptr; ptr += 2;
                stx->sectors[t][s].track = *ptr++;
                stx->sectors[t][s].head = *ptr++;
                stx->sectors[t][s].number = *ptr++;
                stx->sectors[t][s].size = *ptr++;
                stx->sectors[t][s].crc = *(uint16_t*)ptr; ptr += 2;
                stx->sectors[t][s].fdcFlags = *ptr++;
                stx->sectors[t][s].reserved = *ptr++;
            }
        } else {
            stx->sectors[t] = NULL;
        }
        // Skip data
        uint32_t parsed = 16 + ((stx->tracks[t].flags & 0x01) ? 16 * stx->tracks[t].sectorCount : 0);
        ptr += stx->tracks[t].recordSize - parsed;
    }

    free(data);
    return stx;
}

void stx_print(STXFile* stx) {
    printf("File Header:\n");
    printf("  ID: %.4s\n", stx->fileHeader.id);
    printf("  Version: 0x%04X\n", stx->fileHeader.version);
    printf("  Tool: 0x%04X\n", stx->fileHeader.tool);
    printf("  Reserved1: 0x%04X\n", stx->fileHeader.reserved1);
    printf("  Track Count: %u\n", stx->fileHeader.trackCount);
    printf("  Revision: 0x%02X\n", stx->fileHeader.revision);
    printf("  Reserved2: 0x%08X\n", stx->fileHeader.reserved2);

    for (uint8_t t = 0; t < stx->fileHeader.trackCount; t++) {
        printf("\nTrack %u:\n", t);
        printf("  Record Size: %u\n", stx->tracks[t].recordSize);
        printf("  Fuzzy Count: %u\n", stx->tracks[t].fuzzyCount);
        printf("  Sector Count: %u\n", stx->tracks[t].sectorCount);
        printf("  Flags: 0x%04X\n", stx->tracks[t].flags);
        printf("  Length: %u\n", stx->tracks[t].length);
        printf("  Number: %u\n", stx->tracks[t].number);
        printf("  Type: 0x%02X\n", stx->tracks[t].type);

        if (stx->sectors[t]) {
            for (uint16_t s = 0; s < stx->tracks[t].sectorCount; s++) {
                printf("    Sector %u:\n", s);
                printf("      Data Offset: %u\n", stx->sectors[t][s].dataOffset);
                printf("      Bit Position: %u\n", stx->sectors[t][s].bitPosition);
                printf("      Read Time: %u\n", stx->sectors[t][s].readTime);
                printf("      Track: %u\n", stx->sectors[t][s].track);
                printf("      Head: %u\n", stx->sectors[t][s].head);
                printf("      Number: %u\n", stx->sectors[t][s].number);
                printf("      Size: %u\n", stx->sectors[t][s].size);
                printf("      CRC: 0x%04X\n", stx->sectors[t][s].crc);
                printf("      FDC Flags: 0x%02X\n", stx->sectors[t][s].fdcFlags);
                printf("      Reserved: 0x%02X\n", stx->sectors[t][s].reserved);
            }
        }
    }
}

void stx_write(STXFile* stx, const char* newFilepath) {
    // Simplified: write headers; extend for data
    FILE* f = fopen(newFilepath, "wb");
    if (!f) return;

    fwrite(stx->fileHeader.id, 1, 4, f);
    fwrite(&stx->fileHeader.version, 2, 1, f);
    fwrite(&stx->fileHeader.tool, 2, 1, f);
    fwrite(&stx->fileHeader.reserved1, 2, 1, f);
    fwrite(&stx->fileHeader.trackCount, 1, 1, f);
    fwrite(&stx->fileHeader.revision, 1, 1, f);
    fwrite(&stx->fileHeader.reserved2, 4, 1, f);

    for (uint8_t t = 0; t < stx->fileHeader.trackCount; t++) {
        fwrite(&stx->tracks[t].recordSize, 4, 1, f);
        fwrite(&stx->tracks[t].fuzzyCount, 4, 1, f);
        fwrite(&stx->tracks[t].sectorCount, 2, 1, f);
        fwrite(&stx->tracks[t].flags, 2, 1, f);
        fwrite(&stx->tracks[t].length, 2, 1, f);
        fwrite(&stx->tracks[t].number, 1, 1, f);
        fwrite(&stx->tracks[t].type, 1, 1, f);
        if (stx->sectors[t]) {
            for (uint16_t s = 0; s < stx->tracks[t].sectorCount; s++) {
                fwrite(&stx->sectors[t][s].dataOffset, 4, 1, f);
                fwrite(&stx->sectors[t][s].bitPosition, 2, 1, f);
                fwrite(&stx->sectors[t][s].readTime, 2, 1, f);
                fwrite(&stx->sectors[t][s].track, 1, 1, f);
                fwrite(&stx->sectors[t][s].head, 1, 1, f);
                fwrite(&stx->sectors[t][s].number, 1, 1, f);
                fwrite(&stx->sectors[t][s].size, 1, 1, f);
                fwrite(&stx->sectors[t][s].crc, 2, 1, f);
                fwrite(&stx->sectors[t][s].fdcFlags, 1, 1, f);
                fwrite(&stx->sectors[t][s].reserved, 1, 1, f);
            }
        }
        // Add data write if needed
    }
    fclose(f);
}

void stx_close(STXFile* stx) {
    for (uint8_t t = 0; t < stx->fileHeader.trackCount; t++) {
        if (stx->sectors[t]) free(stx->sectors[t]);
    }
    free(stx->sectors);
    free(stx->tracks);
    free(stx);
}

// Example usage:
// int main() {
//     STXFile* stx = stx_open("example.stx");
//     stx_print(stx);
//     stx_write(stx, "example.new.stx");
//     stx_close(stx);
//     return 0;
// }

This C struct-based "class" opens a .STX file, decodes, prints properties to console, and supports basic writing (headers; extend for full data).