Dateien aus D64-Image auslesen

13. Juli 2023 · Programmierung · andreas · Kein Kommentar

altNachdem mit Hilfe des XUM 1541-Adapters die Originalmedien als D64-Images auf dem PC gesichert wurden, steht als nächstes das Auslesen der auf den Disketten befindlichen Dateien an. Es gibt hierzu eine ganze Reihe von Programmen, mit am einfachsten ist wohl das mit dem VICE-Emulator mitgelieferte Kommandozeilenprogramm “c1541”, welches die Aufgabe in einer Zeile erledigt:

$ c1541 -attach Imagename.d64 -extract

Der Spaß am C64 war aber schon immer das Basteln und so war es naheliegend, selbst ein Programm zum Auslesen zu schreiben, dabei etwas Retro-Luft zu schnuppern und längst verblasstes Halbwissen aufzufrischen.

Im Bücherregal steht immer noch eine Ausgabe von “Das große Floppy-Buch” von DATA BECKER, neben “64 Intern” (welches direkt daneben steht) eines der Standardwerke rund um die Programmierung von C64 und Peripherie.

Aufbau eines D64-Images

Zuerst musste aber doch das Internet konsultiert werden, denn zum Zeitpunkt der Drucklegeung konnte noch niemand das erst später entstehende D64-Format voraussehen. Dieses entpuppt sich laut Dokumentation im C64 Wiki aber als recht simpel: alle Blöcke einer Diskette folgen hintereinander, ohne Header- oder Verwaltungsinformationen.

Aus diesem Grund kann das eigentliche Format auch nur anhand der Dateigröße ermittelt werden, für eine Standard-Diskette mit 35 Spuren sind dies 174.848 Byte.

my %d64Variant = (

	174848 => {
		'tracks' => 35,
		'error_info' => 0,
		'blocks' => 683
	},

Sofern Fehlerinformationen vorliegen, kommen nochmal 683 Bytes hinzu, für 40 Spuren sieht der Eintrag wie folgt aus:

	196608 => {
		'tracks' => 40,
		'error_info' => 0,
		'blocks' => 768
	},

Die Anzahl der Bytes pro Block sind immer 256

use constant BYTES_PER_BLOCK => 256;

Aufbau der Diskette

Wie oben bereits erwähnt, ist eine Diskette in unterschiedliche Spuren unterteilt, bei einer nicht modifzierten VC 1541 sind dies 35 Spuren. Die Anzahl an Sektoren nimmt allerdings mit steigender Spurzahl ab:

sub getSectorCount {

	my ($track) = @_;

	my $sectors = 21;

	if ($track >= 18) {
		$sectors -= 2;
	}
	if ($track >= 25) {
		$sectors--;
	}
	if ($track >= 31) {
		$sectors--;
	}

	return $sectors;
}

Somit lässt sich der Start einer Spur innerhalb der Datei wie folgt ermitteln:

sub getTrackStart {

	my ($track) = @_;
	my $start = 0;

	for (my $current = 1; $current < $track; $current++) {

		$start += getSectorCount($current) * BYTES_PER_BLOCK;
	}

	return $start;
}

Ist der Beginn einer Spur gefunden, kann ein Block gelesen werden

sub readBlock {

	my ($track, $sector) = @_;
	my $buffer;

	print "> reading $track / $sector\n";

	seek($fh, getTrackStart($track) + $sector * BYTES_PER_BLOCK, 0);
	read($fh, $buffer, BYTES_PER_BLOCK);

	return $buffer;
}

Block Allocation Map (BAM) und Inhaltsverzeichnis

Die Verwaltungsinformationen einer Diskette beginnen immer bei Spur 18 / Sektor 0, die Position des ersten Verzeichnisblocks ist in den ersten beiden Byte gespeichert: Byte 0 enthält die Spur, Byte 1 den Sektor.

Zu beachten ist, daß bei nicht standardkonformen Disketten die BAM länger als 140 Byte sein kann, um die zusätzlichen Blöcke noch mit unterzubringen - aus diesem Grund wird eine Variable “$header_start” mit dem entsprechenden Wert initialisiert.

Bei der Ausgabe von Text wie z.B. Disketten- oder Dateinamen ist darauf zu achten, daß der Commodore-Zeichensatz zum Einsatz kommt. Für die passende Konvertierungsroutine “petscii_to_ascii” habe ich mich beim Perl-Modul “Text::Convert::PETSCII” bedient.

sub ls {

	my ($fh, $size) = @_;
	my ($buffer, $track, $sector, $entry, $format1, $format2, $name, , $id, $header_start);

	# read BAM, always located at track 18, sector 0
	$buffer = readBlock(18, 0);

	# 0: track of first directory sector
	$track = ord(substr($buffer, 0, 1));
	# 1: sector of first directory sector
	$sector = ord(substr($buffer, 1, 1));

	# 2: disk format (A for standard 1541)
	$format1 = substr($buffer, 2, 1);

	# 4 bytes per track for the BAM
	$header_start = 4 + $d64Variant{$size}->{tracks} * 4;

	# disk name after BAM
	$name = petscii_to_ascii(substr($buffer, $header_start, 18));

	# id after name
	$id = petscii_to_ascii(substr($buffer, $header_start + 18, 2));

	# format after id and 0xa0
	$format2 = substr($buffer, $header_start + 21, 2);

	print "disk name '$name', format '$format1' / '$format2', id '$id'\n";

	do {

		$buffer = readBlock($track, $sector);

		# 0: track of next directory sector
		$track = ord(substr($buffer, 0, 1));
		# 1: sector of next directory sector
		$sector = ord(substr($buffer, 1, 1));

		# directory entries, starting at byte 2, 32 bytes per entry
		for ($entry = 0; $entry < 8; $entry++) {

			lsFile(substr($buffer, 2 + $entry * 32, 30));
		}

	} until (! $track);
}

Jeder Verzeichnisblock ist identisch aufgebaut: sofern im ersten Byte kein $00 steht, folgt nach den aktuellen Einträgen ein weiterer Block, ansonsten endet das Verzeichnis. Es können bis zu 8 Einträge pro Block vorhanden sein, diese starten ab dem zweiten Byte und sind jeweils 32 Byte lang.

my %fType = (

	0 => 'DEL',
	1 => 'SEQ',
	2 => 'PRG',
	3 => 'USR',
	4 => 'REL',
);

sub lsFile {

	my ($buffer) = @_;
	my ($type, $track, $sector, $name, $blocks);

	# 0: file type
	$type = ord(substr($buffer, 0, 1));

	if ($type) {
		# if bit 7 is set, file is write protected
		$type = $type & 0x7f;
		$type = $fType{$type};

		# 1: track
		$track = ord(substr($buffer, 1, 1));
		# 2: sector
		$sector = ord(substr($buffer, 2, 1));

		# 3-18: filename
		$name = substr($buffer, 3, 16);
		$name = petscii_to_ascii($name);

		# 28, 29: size in block(s)
		$blocks = ord(substr($buffer, 28, 1)) + ord(substr($buffer, 29, 1)) * BYTES_PER_BLOCK;

		print "file '$name', type '$type', start: $track/$sector, $blocks block(s)\n";

		if ($blocks && ($type eq 'PRG')) {
			extractFile($name, $type, $track, $sector);
		}
	}

}

In jedem Verzeichniseintrag stecken neben Typ und Name auch Spur und Sektor, in denen der Dateianfang zu finden ist wie auch die Größe der Datei in Blöcken. Zur Ermittlung der tatsächlichen Größe in Byte ist ein komplettes Einlesen aller Blöcke der Datei notwendig.

Auslesen einer PRG-Datei

sub extractFile {

	my ($name, $type, $track, $sector) = @_;
	my ($buffer, $fh);
	my $size = 0;

	# trim spaces
	$name =~ s/^\s+|\s+$//g;
	# replace non-word chars
	$name =~ s/\W/-/g;
	# append type
	$name = lc("$name.$type");

	open($fh, ">$destdir/$name") || die "can't create '$destdir/$name.$type'\n";
	binmode($fh);

	do {

		# read sector
		$buffer = readBlock($track, $sector);

		# 0: next / last track
		$track = ord(substr($buffer, 0, 1));
		# 1: next sector / used bytes
		$sector = ord(substr($buffer, 1, 1));

		# if track > 0 another sector follows
		if ($track) {
			print $fh substr($buffer, 2, BYTES_PER_BLOCK - 2);
			$size += (BYTES_PER_BLOCK - 2);
		}
		# else it's the last sector and $sector denotes the last byte used in the current sector
		else {
			print $fh substr($buffer, 2, $sector - 1);
			$size += ($sector - 1);
		}

	# exit if last sector
	} until (! $track);

	print "$size byte(s) written\n";

	close($fh);
}

Das eigentliche Auslesen einer PRG-Datei ist dann recht einfach zu erledigen:

Jeder Block einer Datei enthält in den ersten beiden Byte die Spur und den Sektor des nächsten Blocks sowie 254 Byte Nutzdaten. Lediglich der letzte Block einer Datei unterscheidet sich hiervon und genau an dieser Stelle hat mich das Floppy-Buch erstmal in die Irre geführt. Dort steht auf Seite 115:

Hier ist das Ende des Programms durch den Wert $00 im Byte $00 gekennzeichnet. Das Byte $01 gibt die Anzahl der Bytes an, die von dem Programm in diesem letzten Block belegt sind.

Das Ergebnis waren Dateien, die jeweils um exakt ein Byte länger waren als die mit c1541 exportierten Dateien. Eine Suche nach der Ursache führte zum Buch “Inside Commodore DOS”, welches auf Seite 49 erläutert:

To signal the DOS that this is the last block, the first byte is set to $00. The first byte is normally the track link. Since there is no track 0, the DOS knows that this is the last sector in the file. The second byte indicates the position of the last byte that is part of the program file.

Somit sind also nur ($sector -1) Byte zu speichern statt $sector Byte und schon sind die ausgelesenen Dateien identisch zu den von c1541 erstellten Dateien.

Mission accomplished!