Inside the Einstein: The Disc, a TatungBytes course: XtalDOS listing the first practice disc image, on a rainbow sunburst
← Back to Modules
module
11

Finding Any Sector

Introduction

Everything the Einstein keeps on a disc - its directory, its files, its own operating system - it keeps in sectors, and it asks for each one by track and sector ID. If edisc.py can do the same, it can read anything on the disc. This section writes the function that does it, and gives the tool its most useful command: dump, which shows you any sector you name.

From Track And ID To A Place In The File

You have every piece already:

  1. Where the track starts - track_offsets(disc)[track] (S9).
  2. Where in the track's list the ID is - the position of the ID in sector_ids(disc, track) (S10).
  3. Where that position's data is - 0x100 + position x 512 from the start of the track (S10).

A list's index gives the position of a value in it: [0, 5, 1, 6].index(1) is 2. So the whole job is three lines:

def find_sector(disc, track, sector_id):
    offset = track_offsets(disc)[track]
    position = sector_ids(disc, track).index(sector_id)
    start = offset + 0x100 + position * 512
    return start

find_sector's sum for track 2, sector 8 of ORDER.DSK, and where the wrong guess lands

find_sector gives back the number of the byte where the sector's data starts; the sector is the 512 bytes from there. dump_sector prints where that is and dumps them, with offsets counted from the start of the sector.

The code

The whole of edisc.py so far. New: find_sector, dump_sector, and the dump command.

import sys


def show_byte(b):
    if 32 <= b < 127:
        return chr(b)
    return "."


def dump(data, start, length):
    for offset in range(start, start + length, 16):
        row = data[offset:offset + 16]
        hex_part = ""
        text_part = ""
        for b in row:
            hex_part = hex_part + f"{b:02X} "
            text_part = text_part + show_byte(b)
        print(f"{offset:06X}  {hex_part:<48} {text_part}")


def read_image(filename):
    with open(filename, "rb") as f:
        return f.read()


def image_format(disc):
    if disc[0:16] == b"EXTENDED CPC DSK":
        return "extended"
    if disc[0:8] == b"MV - CPC":
        return "standard"
    sys.exit("This is not a disc image: it does not start EXTENDED CPC DSK or MV - CPC")


def track_sizes(disc):
    form = image_format(disc)
    count = disc[0x30] * disc[0x31]
    sizes = []
    if form == "extended":
        for i in range(count):
            sizes.append(disc[0x34 + i] * 256)
    else:
        size = int.from_bytes(disc[0x32:0x34], "little")
        for i in range(count):
            sizes.append(size)
    return sizes


def track_offsets(disc):
    offsets = []
    offset = 256
    for size in track_sizes(disc):
        offsets.append(offset)
        offset = offset + size
    return offsets


def sector_ids(disc, track):
    offset = track_offsets(disc)[track]
    count = disc[offset + 0x15]
    ids = []
    for i in range(count):
        entry = offset + 0x18 + i * 8
        ids.append(disc[entry + 2])
    return ids


def find_sector(disc, track, sector_id):
    offset = track_offsets(disc)[track]
    position = sector_ids(disc, track).index(sector_id)
    start = offset + 0x100 + position * 512
    return start


def info(filename, disc):
    sizes = track_sizes(disc)
    expected = 256 + sum(sizes)
    print("Image:   ", filename)
    print("Format:  ", image_format(disc))
    print("Tracks:  ", disc[0x30])
    print("Sides:   ", disc[0x31])
    print("Tracks are", min(sizes), "to", max(sizes), "bytes")
    print("Expected size:", expected, "bytes")
    print("Actual size:  ", len(disc), "bytes")
    if expected == len(disc):
        print("The sizes add up.")
    else:
        print("The sizes DO NOT add up.")


def tracks(disc):
    print("Track  Starts at  Magic  Track Side Sectors  N  Gap3 Filler")
    offsets = track_offsets(disc)
    for i in range(len(offsets)):
        offset = offsets[i]
        header = disc[offset:offset + 256]
        if header[0:12] == b"Track-Info\r\n":
            magic = "yes"
        else:
            magic = "NO"
        print(f"{i:5}  {offset:9}  {magic:5}  {header[0x10]:5} {header[0x11]:4}"
              f" {header[0x15]:7} {header[0x14]:2}  {header[0x16]:02X}   {header[0x17]:02X}")


def sectors(disc, track):
    offset = track_offsets(disc)[track]
    count = disc[offset + 0x15]
    print("Track", track, "has", count, "sectors, stored in this order:")
    print("Position  C  H  ID  N  ST1 ST2  Length  Data starts at")
    for i in range(count):
        entry = disc[offset + 0x18 + i * 8:offset + 0x18 + i * 8 + 8]
        length = int.from_bytes(entry[6:8], "little")
        data_at = offset + 0x100 + i * 512
        print(f"{i:8}  {entry[0]:2} {entry[1]:2} {entry[2]:3} {entry[3]:2}"
              f"  {entry[4]:02X}  {entry[5]:02X}  {length:6}  {data_at}")


def dump_sector(disc, track, sector_id):
    start = find_sector(disc, track, sector_id)
    print(f"Track {track}, sector {sector_id}: bytes {start} to {start + 511} of the image")
    dump(disc[start:start + 512], 0, 512)


command = sys.argv[1]
filename = sys.argv[2]
disc = read_image(filename)

if command == "info":
    info(filename, disc)
elif command == "header":
    dump(disc, 0, 256)
elif command == "tracks":
    tracks(disc)
elif command == "sectors":
    sectors(disc, int(sys.argv[3]))
elif command == "dump":
    dump_sector(disc, int(sys.argv[3]), int(sys.argv[4]))
else:
    sys.exit("Unknown command: " + command)

Run it

python3 edisc.py dump FIRST.DSK 2 4

What you should see (the first eleven rows; the rest are all E5)

Track 2, sector 4: bytes 13312 to 13823 of the image
000000  48 65 6C 6C 6F 20 66 72 6F 6D 20 79 6F 75 72 20  Hello from your 
000010  66 69 72 73 74 20 64 69 73 63 20 69 6D 61 67 65  first disc image
000020  2E 0D 0A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A  ................
000030  1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A  ................
000040  1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A  ................
000050  1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A  ................
000060  1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A  ................
000070  1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A  ................
000080  E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5  ................
000090  E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5  ................
0000A0  E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5  ................

There is HELLO.TXT's text, at byte 13,312 - where S5's search found it. After it, a run of 1A up to offset 0x80, and then the blank disc's E5. Part three explains both: why the text ends where it does, and why 1A.

The Same Question On ORDER.DSK

Now a disc whose sectors are out of order:

python3 edisc.py dump ORDER.DSK 2 8
Track 2, sector 8: bytes 14848 to 15359 of the image
000000  41 20 73 65 63 6F 6E 64 20 66 69 6C 65 2C 20 69  A second file, i
000010  6E 20 62 6C 6F 63 6B 20 32 2E 0D 0A 42 6C 6F 63  n block 2...Bloc
000020  6B 20 32 20 73 74 61 72 74 73 20 6F 6E 20 74 72  k 2 starts on tr
000030  61 63 6B 20 32 0D 0A 61 6E 64 20 65 6E 64 73 20  ack 2..and ends 
000040  6F 6E 20 74 72 61 63 6B 20 33 2E 0D 0A 1A 1A 1A  on track 3......

That is the text the Einstein shows for DISP 1:SECOND.TXT:

0:DISP 1:SECOND.TXT
A second file, in block 2.
Block 2 starts on track 2
and ends on track 3.

Had the tool guessed that sector 8 is at position 8 - at 0x100 + 8 x 512 into the track - it would have read byte 15,360 instead, and found the start of the other file on that disc: This disc's sectors are not stored in order. No error, no warning; just the wrong file. That is why find_sector looks the ID up.

Change one thing

  • python3 edisc.py dump FIRST.DSK 2 0. Among the E5s you will see the name HELLO and TXT. This sector is the start of the disc's directory: the list of what is on it. Part three takes it apart.
  • dump ORDER.DSK 2 0. Which two names are there?
  • Ask for sector 10: dump FIRST.DSK 2 10. What does Python say, and which line of find_sector said it?
  • In find_sector, replace the index line with position = sector_id. Run dump FIRST.DSK 2 4 and dump ORDER.DSK 2 8 again. Which still gives the right answer - and which one now silently gives you the wrong file?

When it goes wrong

SymptomCause
ValueError: 10 is not in listThere is no sector with that ID on the track. Einstein sector IDs are 0 to 9
IndexError: list index out of rangeA track number of 40 or more

Summary

find_sector turns a track and a sector ID into the place in the image where the Einstein's 512 bytes are: the track's start, plus 0x100, plus 512 for each position before the ID's in the sector list. edisc.py dump IMAGE TRACK SECTOR shows any sector. You can now read every byte on an Einstein disc exactly as the Einstein would.

That finishes the container: the wrapping is understood, and from here on the image is simply the disc - 40 tracks of 10 sectors, each reachable by find_sector. What the Einstein keeps in those sectors is Part three.

Next

S12, The Disc's Plan - which sectors hold what: the tracks the Einstein keeps for itself, where the directory lives, and the space left for files.

Get the Newsletter

New guides, disk images and community finds, roughly once a quarter. No spam, we promise, this isn't Tatung's marketing department.
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Newsletter

Subscribe to our newsletter and stay updated.

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.