
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.
You have every piece already:
track_offsets(disc)[track] (S9).sector_ids(disc, track) (S10).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 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 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.
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.
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?dump FIRST.DSK 2 10. What does Python say, and which line of find_sector said it?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?| Symptom | Cause |
|---|---|
ValueError: 10 is not in list | There is no sector with that ID on the track. Einstein sector IDs are 0 to 9 |
IndexError: list index out of range | A track number of 40 or more |
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.
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.
This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.