
create wrote a whole new image. Most writing is smaller than that: change a few bytes of an image that already has files on it, and leave every other byte exactly as it was. This section builds the pieces for that - writing a sector, writing the directory, checking a name, packing an entry - and uses them for the simplest change there is: renaming a file.
Reading a sector was disc[start:start + 512]. Writing one is the same slice on the other side of =:
disc[start:start + 512] = data
That only works on a bytearray - a bytes cannot be changed (S5) - so writing commands turn the image into a bytearray first. And it must be exactly 512 bytes, or every byte after it in the image moves (S5 showed what MAME thinks of that).
write_directory writes the directory's 2,048 bytes back into track 2's sectors 0 to 3: read_directory in reverse.
A name typed on your computer has to fit the disc's rules before it can go in an entry. split_name turns story.txt into the two parts the entry holds, STORY and TXT, and refuses anything that will not fit:
edisc.py. It matches names without caring about case - so it must not create story.txt beside STORY.TXT, which would be two files the Einstein cannot tell apart.*, ? or : - characters the Einstein's own commands use for other things (: after a drive number, as in DIR 1:).split(".") cuts text at each dot into a list: "STORY.TXT".split(".") is ["STORY", "TXT"]. And a function can give back two values at once, as a tuple: return base, ext, received as base, ext = split_name(name).
{ch!r} in an f-string shows a value the way Python would write it, in quotes, so a space in an error message is visible as ' '.
make_entry builds a 32-byte entry from its parts - S13's table, written instead of read. encode("ascii") turns text into bytes, and ljust(8) pads it with spaces to eight. Every byte the function does not set stays 0, as an in-use entry needs. rename does not use it; add, in S22, will.
A locked file (S13) should not be changed by accident. XtalDOS's own rename asks first - File Lock,DRIVE 0: / Unlock (Y/N)?. edisc.py is simpler: check_unlocked refuses to change a locked file at all.
After free_slots:
def write_sector(disc, track, sector_id, data):
start = find_sector(disc, track, sector_id)
disc[start:start + 512] = data
def write_directory(disc, directory):
for sector_id in range(4):
write_sector(disc, 2, sector_id, directory[sector_id * 512:sector_id * 512 + 512])
def split_name(name):
name = name.upper()
parts = name.split(".")
if len(parts) == 1:
parts.append("")
if len(parts) != 2:
sys.exit(f"{name}: a disc name has at most one dot")
base, ext = parts
if len(base) < 1 or len(base) > 8 or len(ext) > 3:
sys.exit(f"{name}: a disc name is 1 to 8 characters, a dot, and up to 3 more")
for ch in base + ext:
if ch <= " " or ch > "~" or ch in "*?:":
sys.exit(f"{name}: the character {ch!r} cannot be used in a disc name")
return base, ext
def make_entry(base, ext, extent, records, blocks):
entry = bytearray(32)
entry[0] = 0
entry[1:9] = base.encode("ascii").ljust(8)
entry[9:12] = ext.encode("ascii").ljust(3)
entry[12] = extent
entry[15] = records
for i in range(len(blocks)):
entry[16 + i * 2:18 + i * 2] = blocks[i].to_bytes(2, "little")
return entry
def check_unlocked(disc, name):
for entry in find_file(disc, name):
if entry[9] & 0x80:
sys.exit(f"{name.upper()} is locked: edisc will not change it")
def name_in_use(disc, name):
for entry in live_entries(disc):
if entry_name(entry).upper() == name.upper():
return True
return False
Two new words join conditions together. or is true if either side is true: len(base) < 1 or len(base) > 8 is true for a name that is too short or too long. and is true only if both sides are, as in rename_file below: the slot must be in use and have the old name.
Text compares by character code, so ch <= " " or ch > "~" catches everything that is not a printable character (S6: 32 to 126). After show_free, the command:
def rename_file(disc, old, new):
check_unlocked(disc, old)
base, ext = split_name(new)
if name_in_use(disc, base + "." + ext):
sys.exit(f"{base}.{ext} is already on this disc")
directory = bytearray(read_directory(disc))
for slot in range(64):
entry = directory[slot * 32:slot * 32 + 32]
if entry[0] != 0xE5 and entry_name(entry).upper() == old.upper():
directory[slot * 32 + 1:slot * 32 + 9] = base.encode("ascii").ljust(8)
directory[slot * 32 + 9:slot * 32 + 12] = ext.encode("ascii").ljust(3)
write_directory(disc, directory)
print(f"Renamed {old.upper()} to {base}.{ext}")
It renames every entry with the old name, so a big file's extents all move together. And before the else::
elif command == "rename":
disc = bytearray(disc)
rename_file(disc, sys.argv[3], sys.argv[4])
save_image(filename, disc)
The image is read, changed in memory, and only then saved - safely, by S19's save_image. If anything stops the program part way, nothing has been written.
(The whole program: s21-edisc.py.)
Run it
Work on a copy, so FILES.DSK stays as it is for later sections: copy FILES.DSK to FILES2.DSK (in your file manager, or with cp on macOS and Linux, copy on Windows). Then:
python3 edisc.py rename FILES2.DSK README.TXT INFO.TXT
python3 edisc.py rename FILES2.DSK story.txt TALE.TXT
python3 edisc.py entries FILES2.DSK
What you should see
README.TXT is locked: edisc will not change it
Renamed STORY.TXT to TALE.TXT
Slot 0: README.TXT status 00 extent 0 records 1 blocks [1] locked
Slot 1: TALE.TXT status 00 extent 0 records 128 blocks [2, 3, 4, 5, 6, 7, 8, 9]
Slot 2: TALE.TXT status 00 extent 1 records 29 blocks [10, 11]
Slot 3: NUMBERS.BIN status 00 extent 0 records 24 blocks [12, 13]
Both of STORY's entries renamed, nothing else touched. And in MAME, with FILES2.DSK in drive 1:
0:DIR 1:
1:*README .TXT : TALE .TXT
1: NUMBERS .BIN
26k Size, 162k Free, 190k Total

Try each of these on FILES2.DSK, and predict the message first:
rename FILES2.DSK TALE.TXT NUMBERS.BINrename FILES2.DSK TALE.TXT TOOLONGNAME.TXTrename FILES2.DSK TALE.TXT "A*B.TXT" (with the quotes: without them, the terminal on a Mac or Linux tries to treat * as a wildcard itself)rename FILES2.DSK TALE.TXT A.B.CThen: in rename_file, take out the right-hand .upper() - the one after old - in entry_name(entry).upper() == old.upper(), and run rename FILES2.DSK tale.txt STORY.TXT. What does the tool print, and what has it actually done? (Run entries.) What would be a better thing for it to say?
| Symptom | Cause |
|---|---|
README.TXT is locked: edisc will not change it | The file's lock bit is set (S13) |
NUMBERS.BIN is already on this disc | Two files cannot share a name |
TOOLONGNAME.TXT: a disc name is 1 to 8 characters, a dot, and up to 3 more | The name does not fit an entry |
A*B.TXT: the character '*' cannot be used in a disc name | *, ? and : are refused |
A.B.C: a disc name has at most one dot | Only one dot, between the name and the extension |
Writing is reading in reverse: slices of a bytearray on the left of =. write_sector and write_directory put bytes back exactly where read_sector and read_directory found them. split_name makes a typed name fit the disc - capitals, 8 and 3, one dot, no awkward characters - and make_entry packs an entry's 32 bytes. rename changes the name in every one of a file's entries, refuses locked files and names already in use, and the Einstein agrees with the result.
S22, add - choosing blocks and a slot, writing a file's data, and its entry: copying a file from your computer onto an Einstein disc.
This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.