[Scummvm-git-logs] scummvm master -> 16d564d826211d64390c146e4ca4c62e5ace4dc6

bluegr noreply at scummvm.org
Sun Aug 25 11:57:11 UTC 2024


This automated email contains information about 6 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .

Summary:
78552492b1 ADL: Move DiskImage into COMMON
f31eb92d7a COMMON: Add DiskImage enhancements
5a7555dfcc COMMON: Add BitArray::size()
23a9f4ed54 COMMON: Add DiskImage lazy-decoding mode
2f2f3a59bd AGI: Apple II NIB and WOZ disk image support
16d564d826 AGI: Map engine error codes to Common codes


Commit: 78552492b1056c02c80eec0d815e1b7d53455079
    https://github.com/scummvm/scummvm/commit/78552492b1056c02c80eec0d815e1b7d53455079
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2024-08-25T14:57:06+03:00

Commit Message:
ADL: Move DiskImage into COMMON

Changed paths:
  A common/formats/disk_image.cpp
  A common/formats/disk_image.h
    common/formats/module.mk
    engines/adl/adl.cpp
    engines/adl/adl.h
    engines/adl/adl_v2.cpp
    engines/adl/adl_v2.h
    engines/adl/adl_v4.cpp
    engines/adl/console.cpp
    engines/adl/disk.cpp
    engines/adl/disk.h
    engines/adl/hires1.cpp
    engines/adl/hires2.cpp
    engines/adl/hires4.cpp
    engines/adl/hires5.cpp
    engines/adl/hires6.cpp


diff --git a/common/formats/disk_image.cpp b/common/formats/disk_image.cpp
new file mode 100644
index 00000000000..f3991c7f5aa
--- /dev/null
+++ b/common/formats/disk_image.cpp
@@ -0,0 +1,784 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/stream.h"
+#include "common/substream.h"
+#include "common/memstream.h"
+#include "common/md5.h"
+#include "common/algorithm.h"
+#include "common/bitstream.h"
+
+#include "common/formats/disk_image.h"
+
+namespace Common {
+
+const uint kNibTrackLen = 256 * 26;
+
+static bool detectDOS33(Common::SeekableReadStream &f, uint size) {
+	uint count = 0;
+	uint dos32 = 0, dos33 = 0;
+	uint32 window = 0;
+
+	while (count++ < size) {
+		window &= 0xffff;
+		window <<= 8;
+		window |= f.readByte();
+
+		if (f.err() || f.eos())
+			return false;
+
+		if (window == 0xd5aa96)
+			++dos33;
+		else if (window == 0xd5aab5)
+			++dos32;
+	}
+
+	return dos33 > dos32;
+}
+
+static bool readSector_NIB(byte outBuf[], uint outBufSize, const byte inBuf[], uint inBufSize, uint &pos, const byte minNibble, const byte lookup[], const uint track, const uint sector) {
+	uint z = inBufSize - (pos % inBufSize);
+	if (z < outBufSize) {
+		memcpy(outBuf, inBuf + (pos % inBufSize), z);
+		memcpy(outBuf + z, inBuf, outBufSize - z);
+	} else
+		memcpy(outBuf, inBuf + (pos % inBufSize), outBufSize);
+	pos += outBufSize;
+
+	byte oldVal = 0;
+	for (uint n = 0; n < outBufSize; ++n) {
+		// expand
+		if (outBuf[n] == 0xd5) {
+			// Early end of block.
+			pos -= (outBufSize - n);
+			debug(2, "NIB: early end of block @ %x (%d, %d)", n, track, sector);
+			return false;
+		}
+
+		byte val = 0x40;
+
+		if (outBuf[n] >= minNibble)
+			val = lookup[outBuf[n] - minNibble];
+
+		if (val == 0x40) {
+			// Badly-encoded nibbles, stop trying to decode here.
+			pos -= (outBufSize - n);
+			debug(2, "NIB: bad nibble %02x @ %x (%d, %d)", outBuf[n], n, track, sector);
+			return false;
+		}
+
+		// undo checksum
+		oldVal = val ^ oldVal;
+		outBuf[n] = oldVal;
+	}
+
+	byte checksum = inBuf[pos++ % inBufSize];
+	if (checksum < minNibble || oldVal != lookup[checksum - minNibble]) {
+		debug(2, "NIB: checksum mismatch @ (%d, %d)", track, sector);
+		return false;
+	}
+
+	return true;
+}
+
+// 4-and-4 encoding (odd-even)
+static uint8 read44(byte *buffer, uint size, uint &pos) {
+	// 1s in the other fields, so we can just AND
+	uint8 ret = buffer[pos++ % size];
+	return ((ret << 1) | 1) & buffer[pos++ % size];
+}
+
+static bool decodeTrack(Common::SeekableReadStream &stream, uint trackLen, bool dos33, byte *const diskImage, uint tracks, Common::Array<bool> &goodSectors) {
+	// starting at 0xaa, 64 is invalid (see below)
+	const byte c_5and3_lookup[] = { 64, 0, 64, 1, 2, 3, 64, 64, 64, 64, 64, 4, 5, 6, 64, 64, 7, 8, 64, 9, 10, 11, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 12, 13, 64, 64, 14, 15, 64, 16, 17, 18, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 19, 20, 64, 21, 22, 23, 64, 64, 64, 64, 64, 24, 25, 26, 64, 64, 27, 28, 64, 29, 30, 31 };
+	// starting at 0x96, 64 is invalid (see below)
+	const byte c_6and2_lookup[] = { 0, 1, 64, 64, 2, 3, 64, 4, 5, 6, 64, 64, 64, 64, 64, 64, 7, 8, 64, 64, 64, 9, 10, 11, 12, 13, 64, 64, 14, 15, 16, 17, 18, 19, 64, 20, 21, 22, 23, 24, 25, 26, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 27, 64, 28, 29, 30, 64, 64, 64, 31, 64, 64, 32, 33, 64, 34, 35, 36, 37, 38, 39, 40, 64, 64, 64, 64, 64, 41, 42, 43, 64, 44, 45, 46, 47, 48, 49, 50, 64, 64, 51, 52, 53, 54, 55, 56, 64, 57, 58, 59, 60, 61, 62, 63 };
+
+	const uint sectorsPerTrack = (dos33 ? 16 : 13);
+	const uint bytesPerSector = 256;
+
+	bool sawAddress = false;
+	uint8 volNo = 0, track = 0, sector = 0;
+
+	byte *const buffer = (byte *)malloc(trackLen);
+	uint firstGoodTrackPos = 0;
+	uint pos = 0;
+
+	if (stream.read(buffer, trackLen) < trackLen) {
+		free(buffer);
+		return false;
+	}
+
+	while (true) {
+		if (pos >= trackLen + firstGoodTrackPos)
+			break;
+
+		// Read until we find two sync bytes.
+		if (buffer[pos++ % trackLen] != 0xd5 || buffer[pos++ % trackLen] != 0xaa)
+			continue;
+
+		byte prologue = buffer[pos++ % trackLen];
+
+		if (sawAddress && prologue == (dos33 ? 0x96 : 0xb5)) {
+			sawAddress = false;
+		}
+
+		if (!sawAddress) {
+			sawAddress = true;
+
+			// We should always find the address field first.
+			if (prologue != (dos33 ? 0x96 : 0xb5)) {
+				// Accept a DOS 3.3(?) header at the start.
+				if (prologue != (dos33 ? 0xb5 : 0x96) && prologue != 0xad && prologue != 0xfd)
+					debug(2, "NIB: unknown field prologue %02x", prologue);
+				sawAddress = false;
+				continue;
+			}
+
+			volNo = read44(buffer, trackLen, pos);
+			track = read44(buffer, trackLen, pos);
+			sector = read44(buffer, trackLen, pos);
+			uint8 checksum = read44(buffer, trackLen, pos);
+			if ((volNo ^ track ^ sector) != checksum) {
+				debug(2, "NIB: invalid checksum (volNo %d, track %d, sector %d)", volNo, track, sector);
+				sawAddress = false;
+				continue;
+			}
+
+			if (track >= tracks || sector >= sectorsPerTrack) {
+				debug(2, "NIB: sector out of bounds @ (%d, %d)", track, sector);
+				sawAddress = false;
+				continue;
+			}
+
+			if (!firstGoodTrackPos)
+				firstGoodTrackPos = pos;
+
+			// Epilogue is de/aa plus a gap, but we don't care.
+			continue;
+		}
+
+		sawAddress = false;
+
+		// We should always find the data field after an address field.
+		// TODO: we ignore volNo?
+		byte *output = diskImage + (track * sectorsPerTrack + sector) * bytesPerSector;
+
+		if (dos33) {
+			// We hardcode the DOS 3.3 mapping here. TODO: Do we also need raw/prodos?
+			int raw2dos[16] = { 0, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 15 };
+			sector = raw2dos[sector];
+			output = diskImage + (track * sectorsPerTrack + sector) * bytesPerSector;
+
+			byte inbuffer[342];
+
+			if (!readSector_NIB(inbuffer, sizeof(inbuffer), buffer, trackLen, pos, 0x96, c_6and2_lookup, track, sector))
+				continue;
+
+			for (uint n = 0; n < 256; ++n) {
+				output[n] = inbuffer[86 + n] << 2;
+				if (n < 86) { // use first pair of bits
+					output[n] |= ((inbuffer[n] & 1) << 1);
+					output[n] |= ((inbuffer[n] & 2) >> 1);
+				} else if (n < 86*2) { // second pair
+					output[n] |= ((inbuffer[n-86] & 4) >> 1);
+					output[n] |= ((inbuffer[n-86] & 8) >> 3);
+				} else { // third pair
+					output[n] |= ((inbuffer[n-86*2] & 0x10) >> 3);
+					output[n] |= ((inbuffer[n-86*2] & 0x20) >> 5);
+				}
+			}
+		} else {
+			// 5-and-3 uses 410 on-disk bytes, decoding to just over 256 bytes
+			byte inbuffer[410];
+
+			if (!readSector_NIB(inbuffer, sizeof(inbuffer), buffer, trackLen, pos, 0xaa, c_5and3_lookup, track, sector))
+				continue;
+
+			// 8 bytes of nibbles expand to 5 bytes
+			// so we have 51 of these batches (255 bytes), plus 2 bytes of 'leftover' nibbles for byte 256
+			for (uint n = 0; n < 51; ++n) {
+				// e.g. figure 3.18 of Beneath Apple DOS
+				byte lowbits1 = inbuffer[51*3 - n];
+				byte lowbits2 = inbuffer[51*2 - n];
+				byte lowbits3 = inbuffer[51*1 - n];
+				byte lowbits4 = (lowbits1 & 2) << 1 | (lowbits2 & 2) | (lowbits3 & 2) >> 1;
+				byte lowbits5 = (lowbits1 & 1) << 2 | (lowbits2 & 1) << 1 | (lowbits3 & 1);
+				output[250 - 5*n] = (inbuffer[n + 51*3 + 1] << 3) | ((lowbits1 >> 2) & 0x7);
+				output[251 - 5*n] = (inbuffer[n + 51*4 + 1] << 3) | ((lowbits2 >> 2) & 0x7);
+				output[252 - 5*n] = (inbuffer[n + 51*5 + 1] << 3) | ((lowbits3 >> 2) & 0x7);
+				output[253 - 5*n] = (inbuffer[n + 51*6 + 1] << 3) | lowbits4;
+				output[254 - 5*n] = (inbuffer[n + 51*7 + 1] << 3) | lowbits5;
+			}
+			output[255] = (inbuffer[409] << 3) | (inbuffer[0] & 0x7);
+		}
+
+		goodSectors[track * sectorsPerTrack + sector] = true;
+	}
+
+	free(buffer);
+	return true;
+}
+
+static void printGoodSectors(Common::Array<bool> &goodSectors, uint sectorsPerTrack) {
+	if (Common::find(goodSectors.begin(), goodSectors.end(), false) != goodSectors.end()) {
+		debugN(1, "NIB: Bad/missing sectors:");
+
+		for (uint i = 0; i < goodSectors.size(); ++i) {
+			if (!goodSectors[i])
+				debugN(1, " (%d, %d)", i / sectorsPerTrack, i % sectorsPerTrack);
+		}
+
+		debugN(1, "\n");
+	}
+}
+
+static Common::SeekableReadStream *readImage_NIB(Common::File &f, bool dos33, uint tracks = 35) {
+	if (f.size() != 35 * kNibTrackLen) {
+		warning("NIB: image '%s' has invalid size of %d bytes", f.getName(), (int)f.size());
+		return nullptr;
+	}
+
+	const uint sectorsPerTrack = (dos33 ? 16 : 13);
+	const uint imageSize = tracks * sectorsPerTrack * 256;
+	byte *const diskImage = (byte *)calloc(imageSize, 1);
+	Common::Array<bool> goodSectors(tracks * sectorsPerTrack);
+
+	for (uint track = 0; track < tracks; ++track) {
+		if (!decodeTrack(f, kNibTrackLen, dos33, diskImage, tracks, goodSectors)) {
+			warning("NIB: error reading '%s'", f.getName());
+			free(diskImage);
+			return nullptr;
+		}
+	}
+
+	printGoodSectors(goodSectors, sectorsPerTrack);
+
+	return new Common::MemoryReadStream(diskImage, imageSize, DisposeAfterUse::YES);
+}
+
+static int getVersion_WOZ(Common::File &f) {
+	f.seek(0);
+	const uint32 fileId = f.readUint32BE();
+
+	if (f.eos() || f.err()) {
+		warning("WOZ: error reading '%s'", f.getName());
+		return 0;
+	}
+
+	if (fileId == MKTAG('W', 'O', 'Z', '1'))
+		return 1;
+	else if (fileId == MKTAG('W', 'O', 'Z', '2'))
+		return 2;
+
+	warning("WOZ: unsupported ID '%s' found in '%s'", tag2str(fileId), f.getName());
+	return 0;
+}
+
+static Common::SeekableReadStream *readTrack_WOZ(Common::File &f, uint track, bool woz2) {
+	f.seek(88 + track * 4);
+	const byte index = f.readByte();
+
+	if (index == 0xff) {
+		warning("WOZ: track %u not found in '%s', skipping", track, f.getName());
+		return nullptr;
+	}
+
+	uint32 offset, byteSize, bitSize;
+
+	if (woz2) {
+		f.seek(256 + index * 8);
+		offset = f.readUint16LE() << 9;
+		byteSize = f.readUint16LE() << 9;
+		bitSize = f.readUint32LE();
+	} else {
+		offset = 256 + index * 6656;
+		f.seek(offset + 6646);
+		byteSize = f.readUint16LE();
+		bitSize = f.readUint16LE();
+	}
+
+	f.seek(offset);
+
+	if (f.eos() || f.err() || byteSize == 0) {
+		warning("WOZ: failed to read track %u in '%s', aborting", track, f.getName());
+		return nullptr;
+	}
+
+	byte *inBuf = (byte *)malloc(byteSize);
+	byte *outBuf = (byte *)malloc(byteSize);
+	uint32 outSize = 0;
+	if (!inBuf || !outBuf) {
+		warning("WOZ: failed to create buffers of size %u for track %u in '%s'", byteSize, track, f.getName());
+		free(inBuf);
+		free(outBuf);
+		return nullptr;
+	}
+
+	if (f.read(inBuf, byteSize) < byteSize) {
+		warning("WOZ: error reading track %u in '%s'", track, f.getName());
+		free(inBuf);
+		free(outBuf);
+		return nullptr;
+	}
+
+	Common::BitStreamMemory8MSB bitStream(new Common::BitStreamMemoryStream(inBuf, byteSize, DisposeAfterUse::YES), DisposeAfterUse::YES);
+
+	byte nibble = 0;
+	bool stop = false;
+	for (;;) {
+		nibble = (nibble << 1) | bitStream.getBit();
+
+		if (nibble & 0x80) {
+			if (stop)
+				break;
+			nibble = 0;
+		}
+
+		if (bitStream.pos() == bitSize) {
+			bitStream.rewind();
+			if (stop) {
+				warning("WOZ: failed to find sync point for track %u in '%s'", track, f.getName());
+				break;
+			}
+			stop = true;
+		}
+	}
+
+	nibble = 0;
+	uint32 bitsRead = 0;
+	do {
+		nibble = (nibble << 1) | bitStream.getBit();
+		++bitsRead;
+
+		if (nibble & 0x80) {
+			outBuf[outSize++] = nibble;
+			nibble = 0;
+		}
+
+		if (bitStream.pos() == bitSize)
+			bitStream.rewind();
+	} while (bitsRead < bitSize);
+
+	if (nibble != 0)
+		warning("WOZ: failed to sync track %u in '%s'", track, f.getName());
+
+	if (outSize == 0) {
+		warning("WOZ: track %u in '%s' is empty", track, f.getName());
+		free(outBuf);
+		return nullptr;
+	}
+
+	return new Common::MemoryReadStream(outBuf, outSize, DisposeAfterUse::YES);
+}
+
+static Common::SeekableReadStream *readImage_WOZ(Common::File &f, bool dos33, uint tracks = 35) {
+	int version = getVersion_WOZ(f);
+
+	if (version == 0)
+		return nullptr;
+
+	const uint sectorsPerTrack = (dos33 ? 16 : 13);
+	const uint imageSize = tracks * sectorsPerTrack * 256;
+	byte *const diskImage = (byte *)calloc(imageSize, 1);
+	Common::Array<bool> goodSectors(tracks * sectorsPerTrack);
+
+	for (uint track = 0; track < tracks; ++track) {
+		StreamPtr stream(readTrack_WOZ(f, track, version == 2));
+
+		if (stream) {
+			if (!decodeTrack(*stream, stream->size(), dos33, diskImage, tracks, goodSectors))
+				error("WOZ: error reading '%s'", f.getName());
+		}
+	}
+
+	printGoodSectors(goodSectors, sectorsPerTrack);
+
+	return new Common::MemoryReadStream(diskImage, imageSize, DisposeAfterUse::YES);
+}
+
+bool DiskImage::open(const Common::Path &filename) {
+	Common::File *f = new Common::File;
+
+	debug(1, "Opening '%s'", filename.toString(Common::Path::kNativeSeparator).c_str());
+
+	if (!f->open(filename)) {
+		warning("Failed to open '%s'", filename.toString(Common::Path::kNativeSeparator).c_str());
+		delete f;
+		return false;
+	}
+
+	Common::String lcName(filename.baseName());
+	lcName.toLowercase();
+
+	if (lcName.hasSuffix(".dsk")) {
+		_tracks = 35;
+		_sectorsPerTrack = 16;
+		_bytesPerSector = 256;
+		_stream = f;
+	} else if (lcName.hasSuffix(".d13")) {
+		_tracks = 35;
+		_sectorsPerTrack = 13;
+		_bytesPerSector = 256;
+		_stream = f;
+	} else if (lcName.hasSuffix(".nib")) {
+		_tracks = 35;
+
+		if (detectDOS33(*f, kNibTrackLen))
+			_sectorsPerTrack = 16;
+		else
+			_sectorsPerTrack = 13;
+
+		_bytesPerSector = 256;
+		f->seek(0);
+		_stream = readImage_NIB(*f, _sectorsPerTrack == 16);
+		delete f;
+	} else if (lcName.hasSuffix(".woz")) {
+		_tracks = 35;
+		_sectorsPerTrack = 13;
+		_bytesPerSector = 256;
+
+		int version = getVersion_WOZ(*f);
+
+		if (version > 0) {
+			StreamPtr bitStream(readTrack_WOZ(*f, 0, version == 2));
+			if (bitStream) {
+				if (detectDOS33(*bitStream, bitStream->size()))
+					_sectorsPerTrack = 16;
+				_stream = readImage_WOZ(*f, _sectorsPerTrack == 16);
+			} else {
+				warning("WOZ: failed to load bitstream for track 0 in '%s'", f->getName());
+			}
+		}
+
+		delete f;
+	} else if (lcName.hasSuffix(".xfd")) {
+		_tracks = 40;
+		_sectorsPerTrack = 18;
+		_bytesPerSector = 128;
+		_stream = f;
+	} else if (lcName.hasSuffix(".img")) {
+		_tracks = 40;
+		_sectorsPerTrack = 8;
+		_bytesPerSector = 512;
+		_firstSector = 1;
+		_stream = f;
+	}
+
+	int expectedSize = _tracks * _sectorsPerTrack * _bytesPerSector;
+
+	if (!_stream)
+		return false;
+
+	if (_stream->size() != expectedSize)
+		error("Unrecognized disk image '%s' of size %d bytes (expected %d bytes)", filename.toString(Common::Path::kNativeSeparator).c_str(), (int)_stream->size(), expectedSize);
+
+	return true;
+}
+
+const DataBlockPtr DiskImage::getDataBlock(uint track, uint sector, uint offset, uint size) const {
+	return DataBlockPtr(new DiskImage::DataBlock(this, track, sector, offset, size, _sectorLimit));
+}
+
+Common::SeekableReadStream *DiskImage::createReadStream(uint track, uint sector, uint offset, uint size, uint sectorLimit) const {
+	const uint bytesToRead = size * _bytesPerSector + _bytesPerSector - offset;
+	byte *const data = (byte *)malloc(bytesToRead);
+	uint dataOffset = 0;
+
+	if (sectorLimit == 0)
+		sectorLimit = _sectorsPerTrack;
+
+	if (sector < _firstSector || sector >= sectorLimit + _firstSector)
+		error("Sector %u is out of bounds for %u-sector %u-based reading", sector, sectorLimit, _firstSector);
+
+	sector -= _firstSector;
+
+	while (dataOffset < bytesToRead) {
+		uint bytesRemInTrack = (sectorLimit - 1 - sector) * _bytesPerSector + _bytesPerSector - offset;
+		_stream->seek((track * _sectorsPerTrack + sector) * _bytesPerSector + offset);
+
+		if (bytesToRead - dataOffset < bytesRemInTrack)
+			bytesRemInTrack = bytesToRead - dataOffset;
+
+		if (_stream->read(data + dataOffset, bytesRemInTrack) < bytesRemInTrack)
+			error("Error reading disk image at track %d; sector %d", track, sector);
+
+		++track;
+
+		sector = 0;
+		offset = 0;
+
+		dataOffset += bytesRemInTrack;
+	}
+
+	return new Common::MemoryReadStream(data, bytesToRead, DisposeAfterUse::YES);
+}
+
+int32 computeMD5(const Common::FSNode &node, Common::String &md5, uint32 md5Bytes) {
+	Common::File f;
+
+	if (!f.open(node))
+		return -1;
+
+	const uint tracks = md5Bytes / (13 * 256) + 1;
+
+	if (node.getName().matchString("*.nib", true) && f.size() == 35 * kNibTrackLen) {
+		bool isDOS33 = detectDOS33(f, kNibTrackLen);
+
+		f.seek(0);
+		Common::SeekableReadStream *stream = readImage_NIB(f, isDOS33, tracks);
+		if (stream) {
+			md5 = Common::computeStreamMD5AsString(*stream, md5Bytes);
+			delete stream;
+			return 35 * (isDOS33 ? 16 : 13) * 256;
+		}
+
+		return -1;
+	} else if (node.getName().matchString("*.woz", true)) {
+		int version = getVersion_WOZ(f);
+
+		if (version > 0) {
+			StreamPtr nibbles(readTrack_WOZ(f, 0, version == 2));
+			if (nibbles) {
+				bool isDOS33 = detectDOS33(*nibbles, nibbles->size());
+				StreamPtr stream(readImage_WOZ(f, isDOS33, tracks));
+				if (stream) {
+					md5 = Common::computeStreamMD5AsString(*stream, md5Bytes);
+					return 35 * (isDOS33 ? 16 : 13) * 256;
+				}
+			}
+		}
+
+		return -1;
+	} else {
+		md5 = Common::computeStreamMD5AsString(f, md5Bytes);
+		return f.size();
+	}
+}
+
+const DataBlockPtr Files_Plain::getDataBlock(const Common::Path &filename, uint offset) const {
+	return Common::SharedPtr<Files::DataBlock>(new Files::DataBlock(this, filename, offset));
+}
+
+Common::SeekableReadStream *Files_Plain::createReadStream(const Common::Path &filename, uint offset) const {
+	Common::File *f(new Common::File());
+
+	if (!f->open(filename))
+		error("Failed to open '%s'", filename.toString(Common::Path::kNativeSeparator).c_str());
+
+	if (offset == 0)
+		return f;
+	else
+		return new Common::SeekableSubReadStream(f, offset, f->size(), DisposeAfterUse::YES);
+}
+
+Files_AppleDOS::~Files_AppleDOS() {
+	delete _disk;
+}
+
+Files_AppleDOS::Files_AppleDOS() :
+		_disk(nullptr) {
+}
+
+void Files_AppleDOS::readSectorList(TrackSector start, Common::Array<TrackSector> &list) {
+	TrackSector index = start;
+
+	while (index.track != 0) {
+		Common::ScopedPtr<Common::SeekableReadStream> stream(_disk->createReadStream(index.track, index.sector));
+
+		stream->readByte();
+		index.track = stream->readByte();
+		index.sector = stream->readByte();
+
+		stream->seek(9, SEEK_CUR);
+
+		// This only handles sequential files
+		TrackSector ts;
+		ts.track = stream->readByte();
+		ts.sector = stream->readByte();
+
+		while (ts.track != 0) {
+			list.push_back(ts);
+
+			ts.track = stream->readByte();
+			ts.sector = stream->readByte();
+
+			if (stream->err())
+				error("Error reading sector list");
+
+			if (stream->eos())
+				break;
+		}
+	}
+}
+
+void Files_AppleDOS::readVTOC() {
+	Common::ScopedPtr<Common::SeekableReadStream> stream(_disk->createReadStream(0x11, 0x00));
+	stream->readByte();
+	byte track = stream->readByte();
+
+	if (!track) {
+		// VTOC probably obfuscated, try track 0x10
+		stream.reset(_disk->createReadStream(0x10, 0x00));
+		stream->readByte();
+		track = stream->readByte();
+	}
+
+	if (!track)
+		error("VTOC not found");
+
+	byte sector = stream->readByte();
+
+	while (track != 0) {
+		char name[kFilenameLen + 1] = { };
+
+		stream.reset(_disk->createReadStream(track, sector));
+		stream->readByte();
+		track = stream->readByte();
+		sector = stream->readByte();
+		stream->seek(8, SEEK_CUR);
+
+		for (uint i = 0; i < 7; ++i) {
+			TOCEntry entry;
+			TrackSector sectorList;
+			sectorList.track = stream->readByte();
+			sectorList.sector = stream->readByte();
+			entry.type = stream->readByte();
+			stream->read(name, kFilenameLen);
+
+			// Convert to ASCII
+			for (uint j = 0; j < kFilenameLen; j++)
+				name[j] &= 0x7f;
+
+			// Strip trailing spaces
+			for (int j = kFilenameLen - 1; j >= 0; --j) {
+				if (name[j] == ' ')
+					name[j] = 0;
+				else
+					break;
+			}
+
+			entry.totalSectors = stream->readUint16BE();
+
+			// 0 is empty slot, 255 is deleted file
+			if (sectorList.track != 0 && sectorList.track != 255) {
+				readSectorList(sectorList, entry.sectors);
+				_toc[name] = entry;
+			}
+		}
+	}
+}
+
+const DataBlockPtr Files_AppleDOS::getDataBlock(const Common::Path &filename, uint offset) const {
+	return Common::SharedPtr<Files::DataBlock>(new Files::DataBlock(this, filename, offset));
+}
+
+Common::SeekableReadStream *Files_AppleDOS::createReadStreamText(const TOCEntry &entry) const {
+	byte *buf = (byte *)malloc(entry.sectors.size() * kSectorSize);
+	byte *p = buf;
+
+	for (uint i = 0; i < entry.sectors.size(); ++i) {
+		Common::ScopedPtr<Common::SeekableReadStream> stream(_disk->createReadStream(entry.sectors[i].track, entry.sectors[i].sector));
+
+		assert(stream->size() == kSectorSize);
+
+		while (true) {
+			byte textChar = stream->readByte();
+
+			if (stream->eos() || textChar == 0)
+				break;
+
+			if (stream->err())
+				error("Error reading text file");
+
+			*p++ = textChar;
+		}
+	}
+
+	return new Common::MemoryReadStream(buf, p - buf, DisposeAfterUse::YES);
+}
+
+Common::SeekableReadStream *Files_AppleDOS::createReadStreamBinary(const TOCEntry &entry) const {
+	byte *buf = (byte *)malloc(entry.sectors.size() * kSectorSize);
+
+	Common::ScopedPtr<Common::SeekableReadStream> stream(_disk->createReadStream(entry.sectors[0].track, entry.sectors[0].sector));
+
+	if (entry.type == kFileTypeBinary)
+		stream->readUint16LE(); // Skip start address
+
+	uint16 size = stream->readUint16LE();
+	uint16 offset = 0;
+	uint16 sectorIdx = 1;
+
+	while (true) {
+		offset += stream->read(buf + offset, size - offset);
+
+		if (offset == size)
+			break;
+
+		if (stream->err())
+			error("Error reading binary file");
+
+		assert(stream->eos());
+
+		if (sectorIdx == entry.sectors.size())
+			error("Not enough sectors for binary file size");
+
+		stream.reset(_disk->createReadStream(entry.sectors[sectorIdx].track, entry.sectors[sectorIdx].sector));
+		++sectorIdx;
+	}
+
+	return new Common::MemoryReadStream(buf, size, DisposeAfterUse::YES);
+}
+
+Common::SeekableReadStream *Files_AppleDOS::createReadStream(const Common::Path &filename, uint offset) const {
+	if (!_toc.contains(filename))
+		error("Failed to locate '%s'", filename.toString().c_str());
+
+	const TOCEntry &entry = _toc[filename];
+
+	Common::SeekableReadStream *stream;
+
+	switch(entry.type) {
+	case kFileTypeText:
+		stream = createReadStreamText(entry);
+		break;
+	case kFileTypeAppleSoft:
+	case kFileTypeBinary:
+		stream = createReadStreamBinary(entry);
+		break;
+	default:
+		error("Unsupported file type %i", entry.type);
+	}
+
+	return new Common::SeekableSubReadStream(stream, offset, stream->size(), DisposeAfterUse::YES);
+}
+
+bool Files_AppleDOS::open(const Common::Path &filename) {
+	_disk = new DiskImage();
+	if (!_disk->open(filename))
+		return false;
+
+	readVTOC();
+	return true;
+}
+
+} // End of namespace Common
diff --git a/common/formats/disk_image.h b/common/formats/disk_image.h
new file mode 100644
index 00000000000..ee7ee6d8477
--- /dev/null
+++ b/common/formats/disk_image.h
@@ -0,0 +1,175 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/ptr.h"
+#include "common/file.h"
+#include "common/debug.h"
+
+#ifndef COMMON_FORMATS_DISK_IMAGE_H
+#define COMMON_FORMATS_DISK_IMAGE_H
+
+namespace Common {
+
+class SeekableReadStream;
+class String;
+
+// Used for disk image detection
+int32 computeMD5(const Common::FSNode &node, Common::String &md5, uint32 md5Bytes);
+
+class DataBlock {
+public:
+	virtual ~DataBlock() { }
+
+	virtual Common::SeekableReadStream *createReadStream() const = 0;
+};
+
+typedef Common::SharedPtr<DataBlock> DataBlockPtr;
+typedef Common::ScopedPtr<Common::SeekableReadStream> StreamPtr;
+
+class Files {
+public:
+	virtual ~Files() { }
+
+	virtual const DataBlockPtr getDataBlock(const Common::Path &filename, uint offset = 0) const = 0;
+	virtual Common::SeekableReadStream *createReadStream(const Common::Path &filename, uint offset = 0) const = 0;
+	virtual bool exists(const Common::Path &filename) const = 0;
+
+protected:
+	class DataBlock : public Common::DataBlock {
+	public:
+		DataBlock(const Files *files, const Common::Path &filename, uint offset) :
+				_files(files),
+				_filename(filename),
+				_offset(offset) { }
+
+		Common::SeekableReadStream *createReadStream() const override {
+			return _files->createReadStream(_filename, _offset);
+		}
+
+	private:
+		const Common::Path _filename;
+		uint _offset;
+		const Files *_files;
+	};
+};
+
+class DiskImage {
+public:
+	DiskImage() :
+			_stream(nullptr),
+			_tracks(0),
+			_sectorsPerTrack(0),
+			_bytesPerSector(0),
+			_sectorLimit(0),
+			_firstSector(0) { }
+
+	~DiskImage() {
+		delete _stream;
+	}
+
+	bool open(const Common::Path &filename);
+	const DataBlockPtr getDataBlock(uint track, uint sector, uint offset = 0, uint size = 0) const;
+	Common::SeekableReadStream *createReadStream(uint track, uint sector, uint offset = 0, uint size = 0, uint sectorsUsed = 0) const;
+	void setSectorLimit(uint sectorLimit) { _sectorLimit = sectorLimit; } // Maximum number of sectors to read per track before stepping
+	uint getBytesPerSector() const { return _bytesPerSector; }
+	uint getSectorsPerTrack() const { return _sectorsPerTrack; }
+	uint getTracks() const { return _tracks; }
+
+protected:
+	class DataBlock : public Common::DataBlock {
+	public:
+		DataBlock(const DiskImage *disk, uint track, uint sector, uint offset, uint size, uint sectorLimit) :
+				_track(track),
+				_sector(sector),
+				_offset(offset),
+				_size(size),
+				_sectorLimit(sectorLimit),
+				_disk(disk) { }
+
+		Common::SeekableReadStream *createReadStream() const override {
+			return _disk->createReadStream(_track, _sector, _offset, _size, _sectorLimit);
+		}
+
+	private:
+		uint _track, _sector, _offset, _size;
+		uint _sectorLimit;
+		const DiskImage *_disk;
+	};
+
+	Common::SeekableReadStream *_stream;
+	uint _tracks, _sectorsPerTrack, _bytesPerSector, _firstSector;
+	uint _sectorLimit;
+};
+
+// Data in plain files
+class Files_Plain : public Files {
+public:
+	const DataBlockPtr getDataBlock(const Common::Path &filename, uint offset = 0) const override;
+	Common::SeekableReadStream *createReadStream(const Common::Path &filename, uint offset = 0) const override;
+	bool exists(const Common::Path &filename) const override { return Common::File::exists(filename); }
+};
+
+// Data in files contained in Apple DOS 3.3 disk image
+class Files_AppleDOS : public Files {
+public:
+	Files_AppleDOS();
+	~Files_AppleDOS() override;
+
+	bool open(const Common::Path &filename);
+	const DataBlockPtr getDataBlock(const Common::Path &filename, uint offset = 0) const override;
+	Common::SeekableReadStream *createReadStream(const Common::Path &filename, uint offset = 0) const override;
+	bool exists(const Common::Path &filename) const override { return _toc.contains(filename); }
+
+private:
+	enum FileType {
+		kFileTypeText = 0,
+		kFileTypeAppleSoft = 2,
+		kFileTypeBinary = 4
+	};
+
+	enum {
+		kSectorSize = 256,
+		kFilenameLen = 30
+	};
+
+	struct TrackSector {
+		byte track;
+		byte sector;
+	};
+
+	struct TOCEntry {
+		byte type;
+		uint16 totalSectors;
+		Common::Array<TrackSector> sectors;
+	};
+
+	void readVTOC();
+	void readSectorList(TrackSector start, Common::Array<TrackSector> &list);
+	Common::SeekableReadStream *createReadStreamText(const TOCEntry &entry) const;
+	Common::SeekableReadStream *createReadStreamBinary(const TOCEntry &entry) const;
+
+	DiskImage *_disk;
+	Common::HashMap<Common::Path, TOCEntry, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> _toc;
+};
+
+} // End of namespace Common
+
+#endif
diff --git a/common/formats/module.mk b/common/formats/module.mk
index c0626152c1d..b15eeaa3c10 100644
--- a/common/formats/module.mk
+++ b/common/formats/module.mk
@@ -2,6 +2,7 @@ MODULE := common/formats
 
 MODULE_OBJS := \
 	cue.o \
+	disk_image.o \
 	formatinfo.o \
 	iff_container.o \
 	ini-file.o \
diff --git a/engines/adl/adl.cpp b/engines/adl/adl.cpp
index dabfacc2798..a9e9895ae11 100644
--- a/engines/adl/adl.cpp
+++ b/engines/adl/adl.cpp
@@ -537,10 +537,10 @@ void AdlEngine::loadDroppedItemOffsets(Common::ReadStream &stream, byte count) {
 
 void AdlEngine::drawPic(byte pic, Common::Point pos) const {
 	if (_roomData.pictures.contains(pic)) {
-		StreamPtr stream(_roomData.pictures[pic]->createReadStream());
+		Common::StreamPtr stream(_roomData.pictures[pic]->createReadStream());
 		_graphics->drawPic(*stream, pos);
 	} else if (_pictures.contains(pic)) {
-		StreamPtr stream(_pictures[pic]->createReadStream());
+		Common::StreamPtr stream(_pictures[pic]->createReadStream());
 		_graphics->drawPic(*stream, pos);
 	} else
 		error("Picture %d not found", pic);
diff --git a/engines/adl/adl.h b/engines/adl/adl.h
index 7d53ecd9f78..a749c65a690 100644
--- a/engines/adl/adl.h
+++ b/engines/adl/adl.h
@@ -107,13 +107,13 @@ struct Room {
 
 	byte description;
 	byte connections[IDI_DIR_TOTAL];
-	DataBlockPtr data;
+	Common::DataBlockPtr data;
 	byte picture;
 	byte curPicture;
 	bool isFirstTime;
 };
 
-typedef Common::HashMap<byte, DataBlockPtr> PictureMap;
+typedef Common::HashMap<byte, Common::DataBlockPtr> PictureMap;
 
 typedef Common::Array<byte> Script;
 
@@ -400,7 +400,7 @@ protected:
 	// Opcodes
 	Common::Array<Opcode> _condOpcodes, _actOpcodes;
 	// Message strings in data file
-	Common::Array<DataBlockPtr> _messages;
+	Common::Array<Common::DataBlockPtr> _messages;
 	// Picture data
 	PictureMap _pictures;
 	// Dropped item screen offsets
diff --git a/engines/adl/adl_v2.cpp b/engines/adl/adl_v2.cpp
index faf4d61a465..c8a76c3e46d 100644
--- a/engines/adl/adl_v2.cpp
+++ b/engines/adl/adl_v2.cpp
@@ -66,7 +66,7 @@ void AdlEngine_v2::mapExeStrings(const Common::StringArray &strings) {
 
 void AdlEngine_v2::insertDisk(byte volume) {
 	delete _disk;
-	_disk = new DiskImage();
+	_disk = new Common::DiskImage();
 
 	if (!_disk->open(getDiskImageName(volume)))
 		error("Failed to open disk volume %d", volume);
@@ -164,7 +164,7 @@ void AdlEngine_v2::handleTextOverflow() {
 
 Common::String AdlEngine_v2::loadMessage(uint idx) const {
 	if (_messages[idx]) {
-		StreamPtr strStream(_messages[idx]->createReadStream());
+		Common::StreamPtr strStream(_messages[idx]->createReadStream());
 		return readString(*strStream, 0xff);
 	}
 
@@ -216,7 +216,7 @@ void AdlEngine_v2::drawItem(Item &item, const Common::Point &pos) {
 		return;
 	}
 
-	StreamPtr stream(_itemPics[item.picture - 1]->createReadStream());
+	Common::StreamPtr stream(_itemPics[item.picture - 1]->createReadStream());
 	stream->readByte(); // Skip clear opcode
 	_graphics->drawPic(*stream, pos);
 }
@@ -231,7 +231,7 @@ void AdlEngine_v2::loadRoom(byte roomNr) {
 	}
 
 	Room &room = getRoom(roomNr);
-	StreamPtr stream(room.data->createReadStream());
+	Common::StreamPtr stream(room.data->createReadStream());
 
 	uint16 descOffset = stream->readUint16LE();
 	uint16 commandOffset = stream->readUint16LE();
@@ -352,7 +352,7 @@ void AdlEngine_v2::drawItems() {
 	}
 }
 
-DataBlockPtr AdlEngine_v2::readDataBlockPtr(Common::ReadStream &f) const {
+Common::DataBlockPtr AdlEngine_v2::readDataBlockPtr(Common::ReadStream &f) const {
 	byte track = f.readByte();
 	byte sector = f.readByte();
 	byte offset = f.readByte();
@@ -362,7 +362,7 @@ DataBlockPtr AdlEngine_v2::readDataBlockPtr(Common::ReadStream &f) const {
 		error("Error reading DataBlockPtr");
 
 	if (track == 0 && sector == 0 && offset == 0 && size == 0)
-		return DataBlockPtr();
+		return Common::DataBlockPtr();
 
 	adjustDataBlockPtr(track, sector, offset, size);
 
diff --git a/engines/adl/adl_v2.h b/engines/adl/adl_v2.h
index 04fdf39787b..18300db8514 100644
--- a/engines/adl/adl_v2.h
+++ b/engines/adl/adl_v2.h
@@ -51,7 +51,7 @@ protected:
 
 	void mapExeStrings(const Common::StringArray &strings);
 	void insertDisk(byte volume);
-	virtual DataBlockPtr readDataBlockPtr(Common::ReadStream &f) const;
+	virtual Common::DataBlockPtr readDataBlockPtr(Common::ReadStream &f) const;
 	virtual void adjustDataBlockPtr(byte &track, byte &sector, byte &offset, byte &size) const { }
 	void loadItems(Common::ReadStream &stream);
 	void loadRooms(Common::ReadStream &stream, byte count);
@@ -87,9 +87,9 @@ protected:
 	} _strings_v2;
 
 	uint _maxLines;
-	DiskImage *_disk;
+	Common::DiskImage *_disk;
 	byte _currentVolume;
-	Common::Array<DataBlockPtr> _itemPics;
+	Common::Array<Common::DataBlockPtr> _itemPics;
 	bool _itemRemoved;
 	byte _roomOnScreen, _picOnScreen, _itemsOnScreen;
 	Common::Array<byte> _brokenRooms;
diff --git a/engines/adl/adl_v4.cpp b/engines/adl/adl_v4.cpp
index 7fdb1d4f6d1..c6c50d10041 100644
--- a/engines/adl/adl_v4.cpp
+++ b/engines/adl/adl_v4.cpp
@@ -176,7 +176,7 @@ void AdlEngine_v4::saveState(Common::WriteStream &stream) {
 
 Common::String AdlEngine_v4::loadMessage(uint idx) const {
 	if (_messages[idx]) {
-		StreamPtr strStream(_messages[idx]->createReadStream());
+		Common::StreamPtr strStream(_messages[idx]->createReadStream());
 		return readString(*strStream, 0xff, "AVISDURGAN");
 	}
 
@@ -293,7 +293,7 @@ void AdlEngine_v4::loadRegion(byte region) {
 	fixupDiskOffset(track, sector);
 
 	for (uint block = 0; block < 7; ++block) {
-		StreamPtr stream(_disk->createReadStream(track, sector, offset, 1));
+		Common::StreamPtr stream(_disk->createReadStream(track, sector, offset, 1));
 
 		uint16 addr = stream->readUint16LE();
 		uint16 size = stream->readUint16LE();
diff --git a/engines/adl/console.cpp b/engines/adl/console.cpp
index 48d7a0c386d..35ea60a01e1 100644
--- a/engines/adl/console.cpp
+++ b/engines/adl/console.cpp
@@ -397,7 +397,7 @@ bool Console::Cmd_ConvertDisk(int argc, const char **argv) {
 		return true;
 	}
 
-	DiskImage inDisk;
+	Common::DiskImage inDisk;
 	if (!inDisk.open(argv[1])) {
 		debugPrintf("Failed to open '%s' for reading\n", argv[1]);
 		return true;
@@ -414,7 +414,7 @@ bool Console::Cmd_ConvertDisk(int argc, const char **argv) {
 
 	byte *const buf = new byte[size];
 
-	StreamPtr stream(inDisk.createReadStream(0, 0, 0, sectors - 1));
+	Common::StreamPtr stream(inDisk.createReadStream(0, 0, 0, sectors - 1));
 	if (stream->read(buf, size) < size) {
 		debugPrintf("Failed to read from stream");
 		delete[] buf;
diff --git a/engines/adl/disk.cpp b/engines/adl/disk.cpp
index 6c830113526..691a83e715e 100644
--- a/engines/adl/disk.cpp
+++ b/engines/adl/disk.cpp
@@ -22,9 +22,8 @@
 #include "common/stream.h"
 #include "common/substream.h"
 #include "common/memstream.h"
-#include "common/md5.h"
-#include "common/algorithm.h"
-#include "common/bitstream.h"
+
+#include "common/formats/disk_image.h"
 
 #include "adl/disk.h"
 
@@ -61,7 +60,7 @@ Common::SeekableReadStream *DataBlock_PC::createReadStream() const {
 	if (_offset == bps - 1)
 		sectors = 1;
 
-	StreamPtr diskStream(_disk->createReadStream(_track, _sector, _offset, sectors));
+	Common::StreamPtr diskStream(_disk->createReadStream(_track, _sector, _offset, sectors));
 
 	byte sizeBuf[2];
 	read(*diskStream, sizeBuf, 2);
@@ -83,755 +82,4 @@ Common::SeekableReadStream *DataBlock_PC::createReadStream() const {
 	return new Common::MemoryReadStream(buf, blockSize, DisposeAfterUse::YES);
 }
 
-const uint kNibTrackLen = 256 * 26;
-
-static bool detectDOS33(Common::SeekableReadStream &f, uint size) {
-	uint count = 0;
-	uint dos32 = 0, dos33 = 0;
-	uint32 window = 0;
-
-	while (count++ < size) {
-		window &= 0xffff;
-		window <<= 8;
-		window |= f.readByte();
-
-		if (f.err() || f.eos())
-			return false;
-
-		if (window == 0xd5aa96)
-			++dos33;
-		else if (window == 0xd5aab5)
-			++dos32;
-	}
-
-	return dos33 > dos32;
-}
-
-static bool readSector_NIB(byte outBuf[], uint outBufSize, const byte inBuf[], uint inBufSize, uint &pos, const byte minNibble, const byte lookup[], const uint track, const uint sector) {
-	uint z = inBufSize - (pos % inBufSize);
-	if (z < outBufSize) {
-		memcpy(outBuf, inBuf + (pos % inBufSize), z);
-		memcpy(outBuf + z, inBuf, outBufSize - z);
-	} else
-		memcpy(outBuf, inBuf + (pos % inBufSize), outBufSize);
-	pos += outBufSize;
-
-	byte oldVal = 0;
-	for (uint n = 0; n < outBufSize; ++n) {
-		// expand
-		if (outBuf[n] == 0xd5) {
-			// Early end of block.
-			pos -= (outBufSize - n);
-			debug(2, "NIB: early end of block @ %x (%d, %d)", n, track, sector);
-			return false;
-		}
-
-		byte val = 0x40;
-
-		if (outBuf[n] >= minNibble)
-			val = lookup[outBuf[n] - minNibble];
-
-		if (val == 0x40) {
-			// Badly-encoded nibbles, stop trying to decode here.
-			pos -= (outBufSize - n);
-			debug(2, "NIB: bad nibble %02x @ %x (%d, %d)", outBuf[n], n, track, sector);
-			return false;
-		}
-
-		// undo checksum
-		oldVal = val ^ oldVal;
-		outBuf[n] = oldVal;
-	}
-
-	byte checksum = inBuf[pos++ % inBufSize];
-	if (checksum < minNibble || oldVal != lookup[checksum - minNibble]) {
-		debug(2, "NIB: checksum mismatch @ (%d, %d)", track, sector);
-		return false;
-	}
-
-	return true;
-}
-
-// 4-and-4 encoding (odd-even)
-static uint8 read44(byte *buffer, uint size, uint &pos) {
-	// 1s in the other fields, so we can just AND
-	uint8 ret = buffer[pos++ % size];
-	return ((ret << 1) | 1) & buffer[pos++ % size];
-}
-
-static bool decodeTrack(Common::SeekableReadStream &stream, uint trackLen, bool dos33, byte *const diskImage, uint tracks, Common::Array<bool> &goodSectors) {
-	// starting at 0xaa, 64 is invalid (see below)
-	const byte c_5and3_lookup[] = { 64, 0, 64, 1, 2, 3, 64, 64, 64, 64, 64, 4, 5, 6, 64, 64, 7, 8, 64, 9, 10, 11, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 12, 13, 64, 64, 14, 15, 64, 16, 17, 18, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 19, 20, 64, 21, 22, 23, 64, 64, 64, 64, 64, 24, 25, 26, 64, 64, 27, 28, 64, 29, 30, 31 };
-	// starting at 0x96, 64 is invalid (see below)
-	const byte c_6and2_lookup[] = { 0, 1, 64, 64, 2, 3, 64, 4, 5, 6, 64, 64, 64, 64, 64, 64, 7, 8, 64, 64, 64, 9, 10, 11, 12, 13, 64, 64, 14, 15, 16, 17, 18, 19, 64, 20, 21, 22, 23, 24, 25, 26, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 27, 64, 28, 29, 30, 64, 64, 64, 31, 64, 64, 32, 33, 64, 34, 35, 36, 37, 38, 39, 40, 64, 64, 64, 64, 64, 41, 42, 43, 64, 44, 45, 46, 47, 48, 49, 50, 64, 64, 51, 52, 53, 54, 55, 56, 64, 57, 58, 59, 60, 61, 62, 63 };
-
-	const uint sectorsPerTrack = (dos33 ? 16 : 13);
-	const uint bytesPerSector = 256;
-
-	bool sawAddress = false;
-	uint8 volNo = 0, track = 0, sector = 0;
-
-	byte *const buffer = (byte *)malloc(trackLen);
-	uint firstGoodTrackPos = 0;
-	uint pos = 0;
-
-	if (stream.read(buffer, trackLen) < trackLen) {
-		free(buffer);
-		return false;
-	}
-
-	while (true) {
-		if (pos >= trackLen + firstGoodTrackPos)
-			break;
-
-		// Read until we find two sync bytes.
-		if (buffer[pos++ % trackLen] != 0xd5 || buffer[pos++ % trackLen] != 0xaa)
-			continue;
-
-		byte prologue = buffer[pos++ % trackLen];
-
-		if (sawAddress && prologue == (dos33 ? 0x96 : 0xb5)) {
-			sawAddress = false;
-		}
-
-		if (!sawAddress) {
-			sawAddress = true;
-
-			// We should always find the address field first.
-			if (prologue != (dos33 ? 0x96 : 0xb5)) {
-				// Accept a DOS 3.3(?) header at the start.
-				if (prologue != (dos33 ? 0xb5 : 0x96) && prologue != 0xad && prologue != 0xfd)
-					debug(2, "NIB: unknown field prologue %02x", prologue);
-				sawAddress = false;
-				continue;
-			}
-
-			volNo = read44(buffer, trackLen, pos);
-			track = read44(buffer, trackLen, pos);
-			sector = read44(buffer, trackLen, pos);
-			uint8 checksum = read44(buffer, trackLen, pos);
-			if ((volNo ^ track ^ sector) != checksum) {
-				debug(2, "NIB: invalid checksum (volNo %d, track %d, sector %d)", volNo, track, sector);
-				sawAddress = false;
-				continue;
-			}
-
-			if (track >= tracks || sector >= sectorsPerTrack) {
-				debug(2, "NIB: sector out of bounds @ (%d, %d)", track, sector);
-				sawAddress = false;
-				continue;
-			}
-
-			if (!firstGoodTrackPos)
-				firstGoodTrackPos = pos;
-
-			// Epilogue is de/aa plus a gap, but we don't care.
-			continue;
-		}
-
-		sawAddress = false;
-
-		// We should always find the data field after an address field.
-		// TODO: we ignore volNo?
-		byte *output = diskImage + (track * sectorsPerTrack + sector) * bytesPerSector;
-
-		if (dos33) {
-			// We hardcode the DOS 3.3 mapping here. TODO: Do we also need raw/prodos?
-			int raw2dos[16] = { 0, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 15 };
-			sector = raw2dos[sector];
-			output = diskImage + (track * sectorsPerTrack + sector) * bytesPerSector;
-
-			byte inbuffer[342];
-
-			if (!readSector_NIB(inbuffer, sizeof(inbuffer), buffer, trackLen, pos, 0x96, c_6and2_lookup, track, sector))
-				continue;
-
-			for (uint n = 0; n < 256; ++n) {
-				output[n] = inbuffer[86 + n] << 2;
-				if (n < 86) { // use first pair of bits
-					output[n] |= ((inbuffer[n] & 1) << 1);
-					output[n] |= ((inbuffer[n] & 2) >> 1);
-				} else if (n < 86*2) { // second pair
-					output[n] |= ((inbuffer[n-86] & 4) >> 1);
-					output[n] |= ((inbuffer[n-86] & 8) >> 3);
-				} else { // third pair
-					output[n] |= ((inbuffer[n-86*2] & 0x10) >> 3);
-					output[n] |= ((inbuffer[n-86*2] & 0x20) >> 5);
-				}
-			}
-		} else {
-			// 5-and-3 uses 410 on-disk bytes, decoding to just over 256 bytes
-			byte inbuffer[410];
-
-			if (!readSector_NIB(inbuffer, sizeof(inbuffer), buffer, trackLen, pos, 0xaa, c_5and3_lookup, track, sector))
-				continue;
-
-			// 8 bytes of nibbles expand to 5 bytes
-			// so we have 51 of these batches (255 bytes), plus 2 bytes of 'leftover' nibbles for byte 256
-			for (uint n = 0; n < 51; ++n) {
-				// e.g. figure 3.18 of Beneath Apple DOS
-				byte lowbits1 = inbuffer[51*3 - n];
-				byte lowbits2 = inbuffer[51*2 - n];
-				byte lowbits3 = inbuffer[51*1 - n];
-				byte lowbits4 = (lowbits1 & 2) << 1 | (lowbits2 & 2) | (lowbits3 & 2) >> 1;
-				byte lowbits5 = (lowbits1 & 1) << 2 | (lowbits2 & 1) << 1 | (lowbits3 & 1);
-				output[250 - 5*n] = (inbuffer[n + 51*3 + 1] << 3) | ((lowbits1 >> 2) & 0x7);
-				output[251 - 5*n] = (inbuffer[n + 51*4 + 1] << 3) | ((lowbits2 >> 2) & 0x7);
-				output[252 - 5*n] = (inbuffer[n + 51*5 + 1] << 3) | ((lowbits3 >> 2) & 0x7);
-				output[253 - 5*n] = (inbuffer[n + 51*6 + 1] << 3) | lowbits4;
-				output[254 - 5*n] = (inbuffer[n + 51*7 + 1] << 3) | lowbits5;
-			}
-			output[255] = (inbuffer[409] << 3) | (inbuffer[0] & 0x7);
-		}
-
-		goodSectors[track * sectorsPerTrack + sector] = true;
-	}
-
-	free(buffer);
-	return true;
-}
-
-static void printGoodSectors(Common::Array<bool> &goodSectors, uint sectorsPerTrack) {
-	if (Common::find(goodSectors.begin(), goodSectors.end(), false) != goodSectors.end()) {
-		debugN(1, "NIB: Bad/missing sectors:");
-
-		for (uint i = 0; i < goodSectors.size(); ++i) {
-			if (!goodSectors[i])
-				debugN(1, " (%d, %d)", i / sectorsPerTrack, i % sectorsPerTrack);
-		}
-
-		debugN(1, "\n");
-	}
-}
-
-static Common::SeekableReadStream *readImage_NIB(Common::File &f, bool dos33, uint tracks = 35) {
-	if (f.size() != 35 * kNibTrackLen) {
-		warning("NIB: image '%s' has invalid size of %d bytes", f.getName(), (int)f.size());
-		return nullptr;
-	}
-
-	const uint sectorsPerTrack = (dos33 ? 16 : 13);
-	const uint imageSize = tracks * sectorsPerTrack * 256;
-	byte *const diskImage = (byte *)calloc(imageSize, 1);
-	Common::Array<bool> goodSectors(tracks * sectorsPerTrack);
-
-	for (uint track = 0; track < tracks; ++track) {
-		if (!decodeTrack(f, kNibTrackLen, dos33, diskImage, tracks, goodSectors)) {
-			warning("NIB: error reading '%s'", f.getName());
-			free(diskImage);
-			return nullptr;
-		}
-	}
-
-	printGoodSectors(goodSectors, sectorsPerTrack);
-
-	return new Common::MemoryReadStream(diskImage, imageSize, DisposeAfterUse::YES);
-}
-
-static int getVersion_WOZ(Common::File &f) {
-	f.seek(0);
-	const uint32 fileId = f.readUint32BE();
-
-	if (f.eos() || f.err()) {
-		warning("WOZ: error reading '%s'", f.getName());
-		return 0;
-	}
-
-	if (fileId == MKTAG('W', 'O', 'Z', '1'))
-		return 1;
-	else if (fileId == MKTAG('W', 'O', 'Z', '2'))
-		return 2;
-
-	warning("WOZ: unsupported ID '%s' found in '%s'", tag2str(fileId), f.getName());
-	return 0;
-}
-
-static Common::SeekableReadStream *readTrack_WOZ(Common::File &f, uint track, bool woz2) {
-	f.seek(88 + track * 4);
-	const byte index = f.readByte();
-
-	if (index == 0xff) {
-		warning("WOZ: track %u not found in '%s', skipping", track, f.getName());
-		return nullptr;
-	}
-
-	uint32 offset, byteSize, bitSize;
-
-	if (woz2) {
-		f.seek(256 + index * 8);
-		offset = f.readUint16LE() << 9;
-		byteSize = f.readUint16LE() << 9;
-		bitSize = f.readUint32LE();
-	} else {
-		offset = 256 + index * 6656;
-		f.seek(offset + 6646);
-		byteSize = f.readUint16LE();
-		bitSize = f.readUint16LE();
-	}
-
-	f.seek(offset);
-
-	if (f.eos() || f.err() || byteSize == 0) {
-		warning("WOZ: failed to read track %u in '%s', aborting", track, f.getName());
-		return nullptr;
-	}
-
-	byte *inBuf = (byte *)malloc(byteSize);
-	byte *outBuf = (byte *)malloc(byteSize);
-	uint32 outSize = 0;
-	if (!inBuf || !outBuf) {
-		warning("WOZ: failed to create buffers of size %u for track %u in '%s'", byteSize, track, f.getName());
-		free(inBuf);
-		free(outBuf);
-		return nullptr;
-	}
-
-	if (f.read(inBuf, byteSize) < byteSize) {
-		warning("WOZ: error reading track %u in '%s'", track, f.getName());
-		free(inBuf);
-		free(outBuf);
-		return nullptr;
-	}
-
-	Common::BitStreamMemory8MSB bitStream(new Common::BitStreamMemoryStream(inBuf, byteSize, DisposeAfterUse::YES), DisposeAfterUse::YES);
-
-	byte nibble = 0;
-	bool stop = false;
-	for (;;) {
-		nibble = (nibble << 1) | bitStream.getBit();
-
-		if (nibble & 0x80) {
-			if (stop)
-				break;
-			nibble = 0;
-		}
-
-		if (bitStream.pos() == bitSize) {
-			bitStream.rewind();
-			if (stop) {
-				warning("WOZ: failed to find sync point for track %u in '%s'", track, f.getName());
-				break;
-			}
-			stop = true;
-		}
-	}
-
-	nibble = 0;
-	uint32 bitsRead = 0;
-	do {
-		nibble = (nibble << 1) | bitStream.getBit();
-		++bitsRead;
-
-		if (nibble & 0x80) {
-			outBuf[outSize++] = nibble;
-			nibble = 0;
-		}
-
-		if (bitStream.pos() == bitSize)
-			bitStream.rewind();
-	} while (bitsRead < bitSize);
-
-	if (nibble != 0)
-		warning("WOZ: failed to sync track %u in '%s'", track, f.getName());
-
-	if (outSize == 0) {
-		warning("WOZ: track %u in '%s' is empty", track, f.getName());
-		free(outBuf);
-		return nullptr;
-	}
-
-	return new Common::MemoryReadStream(outBuf, outSize, DisposeAfterUse::YES);
-}
-
-static Common::SeekableReadStream *readImage_WOZ(Common::File &f, bool dos33, uint tracks = 35) {
-	int version = getVersion_WOZ(f);
-
-	if (version == 0)
-		return nullptr;
-
-	const uint sectorsPerTrack = (dos33 ? 16 : 13);
-	const uint imageSize = tracks * sectorsPerTrack * 256;
-	byte *const diskImage = (byte *)calloc(imageSize, 1);
-	Common::Array<bool> goodSectors(tracks * sectorsPerTrack);
-
-	for (uint track = 0; track < tracks; ++track) {
-		StreamPtr stream(readTrack_WOZ(f, track, version == 2));
-
-		if (stream) {
-			if (!decodeTrack(*stream, stream->size(), dos33, diskImage, tracks, goodSectors))
-				error("WOZ: error reading '%s'", f.getName());
-		}
-	}
-
-	printGoodSectors(goodSectors, sectorsPerTrack);
-
-	return new Common::MemoryReadStream(diskImage, imageSize, DisposeAfterUse::YES);
-}
-
-bool DiskImage::open(const Common::Path &filename) {
-	Common::File *f = new Common::File;
-
-	debug(1, "Opening '%s'", filename.toString(Common::Path::kNativeSeparator).c_str());
-
-	if (!f->open(filename)) {
-		warning("Failed to open '%s'", filename.toString(Common::Path::kNativeSeparator).c_str());
-		delete f;
-		return false;
-	}
-
-	Common::String lcName(filename.baseName());
-	lcName.toLowercase();
-
-	if (lcName.hasSuffix(".dsk")) {
-		_tracks = 35;
-		_sectorsPerTrack = 16;
-		_bytesPerSector = 256;
-		_stream = f;
-	} else if (lcName.hasSuffix(".d13")) {
-		_tracks = 35;
-		_sectorsPerTrack = 13;
-		_bytesPerSector = 256;
-		_stream = f;
-	} else if (lcName.hasSuffix(".nib")) {
-		_tracks = 35;
-
-		if (detectDOS33(*f, kNibTrackLen))
-			_sectorsPerTrack = 16;
-		else
-			_sectorsPerTrack = 13;
-
-		_bytesPerSector = 256;
-		f->seek(0);
-		_stream = readImage_NIB(*f, _sectorsPerTrack == 16);
-		delete f;
-	} else if (lcName.hasSuffix(".woz")) {
-		_tracks = 35;
-		_sectorsPerTrack = 13;
-		_bytesPerSector = 256;
-
-		int version = getVersion_WOZ(*f);
-
-		if (version > 0) {
-			StreamPtr bitStream(readTrack_WOZ(*f, 0, version == 2));
-			if (bitStream) {
-				if (detectDOS33(*bitStream, bitStream->size()))
-					_sectorsPerTrack = 16;
-				_stream = readImage_WOZ(*f, _sectorsPerTrack == 16);
-			} else {
-				warning("WOZ: failed to load bitstream for track 0 in '%s'", f->getName());
-			}
-		}
-
-		delete f;
-	} else if (lcName.hasSuffix(".xfd")) {
-		_tracks = 40;
-		_sectorsPerTrack = 18;
-		_bytesPerSector = 128;
-		_stream = f;
-	} else if (lcName.hasSuffix(".img")) {
-		_tracks = 40;
-		_sectorsPerTrack = 8;
-		_bytesPerSector = 512;
-		_firstSector = 1;
-		_stream = f;
-	}
-
-	int expectedSize = _tracks * _sectorsPerTrack * _bytesPerSector;
-
-	if (!_stream)
-		return false;
-
-	if (_stream->size() != expectedSize)
-		error("Unrecognized disk image '%s' of size %d bytes (expected %d bytes)", filename.toString(Common::Path::kNativeSeparator).c_str(), (int)_stream->size(), expectedSize);
-
-	return true;
-}
-
-const DataBlockPtr DiskImage::getDataBlock(uint track, uint sector, uint offset, uint size) const {
-	return DataBlockPtr(new DiskImage::DataBlock(this, track, sector, offset, size, _sectorLimit));
-}
-
-Common::SeekableReadStream *DiskImage::createReadStream(uint track, uint sector, uint offset, uint size, uint sectorLimit) const {
-	const uint bytesToRead = size * _bytesPerSector + _bytesPerSector - offset;
-	byte *const data = (byte *)malloc(bytesToRead);
-	uint dataOffset = 0;
-
-	if (sectorLimit == 0)
-		sectorLimit = _sectorsPerTrack;
-
-	if (sector < _firstSector || sector >= sectorLimit + _firstSector)
-		error("Sector %u is out of bounds for %u-sector %u-based reading", sector, sectorLimit, _firstSector);
-
-	sector -= _firstSector;
-
-	while (dataOffset < bytesToRead) {
-		uint bytesRemInTrack = (sectorLimit - 1 - sector) * _bytesPerSector + _bytesPerSector - offset;
-		_stream->seek((track * _sectorsPerTrack + sector) * _bytesPerSector + offset);
-
-		if (bytesToRead - dataOffset < bytesRemInTrack)
-			bytesRemInTrack = bytesToRead - dataOffset;
-
-		if (_stream->read(data + dataOffset, bytesRemInTrack) < bytesRemInTrack)
-			error("Error reading disk image at track %d; sector %d", track, sector);
-
-		++track;
-
-		sector = 0;
-		offset = 0;
-
-		dataOffset += bytesRemInTrack;
-	}
-
-	return new Common::MemoryReadStream(data, bytesToRead, DisposeAfterUse::YES);
-}
-
-int32 computeMD5(const Common::FSNode &node, Common::String &md5, uint32 md5Bytes) {
-	Common::File f;
-
-	if (!f.open(node))
-		return -1;
-
-	const uint tracks = md5Bytes / (13 * 256) + 1;
-
-	if (node.getName().matchString("*.nib", true) && f.size() == 35 * kNibTrackLen) {
-		bool isDOS33 = detectDOS33(f, kNibTrackLen);
-
-		f.seek(0);
-		Common::SeekableReadStream *stream = readImage_NIB(f, isDOS33, tracks);
-		if (stream) {
-			md5 = Common::computeStreamMD5AsString(*stream, md5Bytes);
-			delete stream;
-			return 35 * (isDOS33 ? 16 : 13) * 256;
-		}
-
-		return -1;
-	} else if (node.getName().matchString("*.woz", true)) {
-		int version = getVersion_WOZ(f);
-
-		if (version > 0) {
-			StreamPtr nibbles(readTrack_WOZ(f, 0, version == 2));
-			if (nibbles) {
-				bool isDOS33 = detectDOS33(*nibbles, nibbles->size());
-				StreamPtr stream(readImage_WOZ(f, isDOS33, tracks));
-				if (stream) {
-					md5 = Common::computeStreamMD5AsString(*stream, md5Bytes);
-					return 35 * (isDOS33 ? 16 : 13) * 256;
-				}
-			}
-		}
-
-		return -1;
-	} else {
-		md5 = Common::computeStreamMD5AsString(f, md5Bytes);
-		return f.size();
-	}
-}
-
-const DataBlockPtr Files_Plain::getDataBlock(const Common::Path &filename, uint offset) const {
-	return Common::SharedPtr<Files::DataBlock>(new Files::DataBlock(this, filename, offset));
-}
-
-Common::SeekableReadStream *Files_Plain::createReadStream(const Common::Path &filename, uint offset) const {
-	Common::File *f(new Common::File());
-
-	if (!f->open(filename))
-		error("Failed to open '%s'", filename.toString(Common::Path::kNativeSeparator).c_str());
-
-	if (offset == 0)
-		return f;
-	else
-		return new Common::SeekableSubReadStream(f, offset, f->size(), DisposeAfterUse::YES);
-}
-
-Files_AppleDOS::~Files_AppleDOS() {
-	delete _disk;
-}
-
-Files_AppleDOS::Files_AppleDOS() :
-		_disk(nullptr) {
-}
-
-void Files_AppleDOS::readSectorList(TrackSector start, Common::Array<TrackSector> &list) {
-	TrackSector index = start;
-
-	while (index.track != 0) {
-		Common::ScopedPtr<Common::SeekableReadStream> stream(_disk->createReadStream(index.track, index.sector));
-
-		stream->readByte();
-		index.track = stream->readByte();
-		index.sector = stream->readByte();
-
-		stream->seek(9, SEEK_CUR);
-
-		// This only handles sequential files
-		TrackSector ts;
-		ts.track = stream->readByte();
-		ts.sector = stream->readByte();
-
-		while (ts.track != 0) {
-			list.push_back(ts);
-
-			ts.track = stream->readByte();
-			ts.sector = stream->readByte();
-
-			if (stream->err())
-				error("Error reading sector list");
-
-			if (stream->eos())
-				break;
-		}
-	}
-}
-
-void Files_AppleDOS::readVTOC() {
-	Common::ScopedPtr<Common::SeekableReadStream> stream(_disk->createReadStream(0x11, 0x00));
-	stream->readByte();
-	byte track = stream->readByte();
-
-	if (!track) {
-		// VTOC probably obfuscated, try track 0x10
-		stream.reset(_disk->createReadStream(0x10, 0x00));
-		stream->readByte();
-		track = stream->readByte();
-	}
-
-	if (!track)
-		error("VTOC not found");
-
-	byte sector = stream->readByte();
-
-	while (track != 0) {
-		char name[kFilenameLen + 1] = { };
-
-		stream.reset(_disk->createReadStream(track, sector));
-		stream->readByte();
-		track = stream->readByte();
-		sector = stream->readByte();
-		stream->seek(8, SEEK_CUR);
-
-		for (uint i = 0; i < 7; ++i) {
-			TOCEntry entry;
-			TrackSector sectorList;
-			sectorList.track = stream->readByte();
-			sectorList.sector = stream->readByte();
-			entry.type = stream->readByte();
-			stream->read(name, kFilenameLen);
-
-			// Convert to ASCII
-			for (uint j = 0; j < kFilenameLen; j++)
-				name[j] &= 0x7f;
-
-			// Strip trailing spaces
-			for (int j = kFilenameLen - 1; j >= 0; --j) {
-				if (name[j] == ' ')
-					name[j] = 0;
-				else
-					break;
-			}
-
-			entry.totalSectors = stream->readUint16BE();
-
-			// 0 is empty slot, 255 is deleted file
-			if (sectorList.track != 0 && sectorList.track != 255) {
-				readSectorList(sectorList, entry.sectors);
-				_toc[name] = entry;
-			}
-		}
-	}
-}
-
-const DataBlockPtr Files_AppleDOS::getDataBlock(const Common::Path &filename, uint offset) const {
-	return Common::SharedPtr<Files::DataBlock>(new Files::DataBlock(this, filename, offset));
-}
-
-Common::SeekableReadStream *Files_AppleDOS::createReadStreamText(const TOCEntry &entry) const {
-	byte *buf = (byte *)malloc(entry.sectors.size() * kSectorSize);
-	byte *p = buf;
-
-	for (uint i = 0; i < entry.sectors.size(); ++i) {
-		Common::ScopedPtr<Common::SeekableReadStream> stream(_disk->createReadStream(entry.sectors[i].track, entry.sectors[i].sector));
-
-		assert(stream->size() == kSectorSize);
-
-		while (true) {
-			byte textChar = stream->readByte();
-
-			if (stream->eos() || textChar == 0)
-				break;
-
-			if (stream->err())
-				error("Error reading text file");
-
-			*p++ = textChar;
-		}
-	}
-
-	return new Common::MemoryReadStream(buf, p - buf, DisposeAfterUse::YES);
-}
-
-Common::SeekableReadStream *Files_AppleDOS::createReadStreamBinary(const TOCEntry &entry) const {
-	byte *buf = (byte *)malloc(entry.sectors.size() * kSectorSize);
-
-	Common::ScopedPtr<Common::SeekableReadStream> stream(_disk->createReadStream(entry.sectors[0].track, entry.sectors[0].sector));
-
-	if (entry.type == kFileTypeBinary)
-		stream->readUint16LE(); // Skip start address
-
-	uint16 size = stream->readUint16LE();
-	uint16 offset = 0;
-	uint16 sectorIdx = 1;
-
-	while (true) {
-		offset += stream->read(buf + offset, size - offset);
-
-		if (offset == size)
-			break;
-
-		if (stream->err())
-			error("Error reading binary file");
-
-		assert(stream->eos());
-
-		if (sectorIdx == entry.sectors.size())
-			error("Not enough sectors for binary file size");
-
-		stream.reset(_disk->createReadStream(entry.sectors[sectorIdx].track, entry.sectors[sectorIdx].sector));
-		++sectorIdx;
-	}
-
-	return new Common::MemoryReadStream(buf, size, DisposeAfterUse::YES);
-}
-
-Common::SeekableReadStream *Files_AppleDOS::createReadStream(const Common::Path &filename, uint offset) const {
-	if (!_toc.contains(filename))
-		error("Failed to locate '%s'", filename.toString().c_str());
-
-	const TOCEntry &entry = _toc[filename];
-
-	Common::SeekableReadStream *stream;
-
-	switch(entry.type) {
-	case kFileTypeText:
-		stream = createReadStreamText(entry);
-		break;
-	case kFileTypeAppleSoft:
-	case kFileTypeBinary:
-		stream = createReadStreamBinary(entry);
-		break;
-	default:
-		error("Unsupported file type %i", entry.type);
-	}
-
-	return new Common::SeekableSubReadStream(stream, offset, stream->size(), DisposeAfterUse::YES);
-}
-
-bool Files_AppleDOS::open(const Common::Path &filename) {
-	_disk = new DiskImage();
-	if (!_disk->open(filename))
-		return false;
-
-	readVTOC();
-	return true;
-}
-
-} // End of namespace Adl
+} // End of namespace Common
diff --git a/engines/adl/disk.h b/engines/adl/disk.h
index a3ddc64f4f4..1bf49cc7576 100644
--- a/engines/adl/disk.h
+++ b/engines/adl/disk.h
@@ -23,6 +23,8 @@
 #include "common/file.h"
 #include "common/debug.h"
 
+#include "common/formats/disk_image.h"
+
 #ifndef ADL_DISK_H
 #define ADL_DISK_H
 
@@ -33,153 +35,14 @@ class String;
 
 namespace Adl {
 
-// Used for disk image detection
-int32 computeMD5(const Common::FSNode &node, Common::String &md5, uint32 md5Bytes);
-
-class DataBlock {
-public:
-	virtual ~DataBlock() { }
-
-	virtual Common::SeekableReadStream *createReadStream() const = 0;
-};
-
-typedef Common::SharedPtr<DataBlock> DataBlockPtr;
-typedef Common::ScopedPtr<Common::SeekableReadStream> StreamPtr;
-
-class Files {
-public:
-	virtual ~Files() { }
-
-	virtual const DataBlockPtr getDataBlock(const Common::Path &filename, uint offset = 0) const = 0;
-	virtual Common::SeekableReadStream *createReadStream(const Common::Path &filename, uint offset = 0) const = 0;
-	virtual bool exists(const Common::Path &filename) const = 0;
-
-protected:
-	class DataBlock : public Adl::DataBlock {
-	public:
-		DataBlock(const Files *files, const Common::Path &filename, uint offset) :
-				_files(files),
-				_filename(filename),
-				_offset(offset) { }
-
-		Common::SeekableReadStream *createReadStream() const override {
-			return _files->createReadStream(_filename, _offset);
-		}
-
-	private:
-		const Common::Path _filename;
-		uint _offset;
-		const Files *_files;
-	};
-};
-
-class DiskImage {
-public:
-	DiskImage() :
-			_stream(nullptr),
-			_tracks(0),
-			_sectorsPerTrack(0),
-			_bytesPerSector(0),
-			_sectorLimit(0),
-			_firstSector(0) { }
-
-	~DiskImage() {
-		delete _stream;
-	}
-
-	bool open(const Common::Path &filename);
-	const DataBlockPtr getDataBlock(uint track, uint sector, uint offset = 0, uint size = 0) const;
-	Common::SeekableReadStream *createReadStream(uint track, uint sector, uint offset = 0, uint size = 0, uint sectorsUsed = 0) const;
-	void setSectorLimit(uint sectorLimit) { _sectorLimit = sectorLimit; } // Maximum number of sectors to read per track before stepping
-	uint getBytesPerSector() const { return _bytesPerSector; }
-	uint getSectorsPerTrack() const { return _sectorsPerTrack; }
-	uint getTracks() const { return _tracks; }
-
-protected:
-	class DataBlock : public Adl::DataBlock {
-	public:
-		DataBlock(const DiskImage *disk, uint track, uint sector, uint offset, uint size, uint sectorLimit) :
-				_track(track),
-				_sector(sector),
-				_offset(offset),
-				_size(size),
-				_sectorLimit(sectorLimit),
-				_disk(disk) { }
-
-		Common::SeekableReadStream *createReadStream() const override {
-			return _disk->createReadStream(_track, _sector, _offset, _size, _sectorLimit);
-		}
-
-	private:
-		uint _track, _sector, _offset, _size;
-		uint _sectorLimit;
-		const DiskImage *_disk;
-	};
-
-	Common::SeekableReadStream *_stream;
-	uint _tracks, _sectorsPerTrack, _bytesPerSector, _firstSector;
-	uint _sectorLimit;
-};
-
-// Data in plain files
-class Files_Plain : public Files {
-public:
-	const DataBlockPtr getDataBlock(const Common::Path &filename, uint offset = 0) const override;
-	Common::SeekableReadStream *createReadStream(const Common::Path &filename, uint offset = 0) const override;
-	bool exists(const Common::Path &filename) const override { return Common::File::exists(filename); }
-};
-
-// Data in files contained in Apple DOS 3.3 disk image
-class Files_AppleDOS : public Files {
-public:
-	Files_AppleDOS();
-	~Files_AppleDOS() override;
-
-	bool open(const Common::Path &filename);
-	const DataBlockPtr getDataBlock(const Common::Path &filename, uint offset = 0) const override;
-	Common::SeekableReadStream *createReadStream(const Common::Path &filename, uint offset = 0) const override;
-	bool exists(const Common::Path &filename) const override { return _toc.contains(filename); }
-
-private:
-	enum FileType {
-		kFileTypeText = 0,
-		kFileTypeAppleSoft = 2,
-		kFileTypeBinary = 4
-	};
-
-	enum {
-		kSectorSize = 256,
-		kFilenameLen = 30
-	};
-
-	struct TrackSector {
-		byte track;
-		byte sector;
-	};
-
-	struct TOCEntry {
-		byte type;
-		uint16 totalSectors;
-		Common::Array<TrackSector> sectors;
-	};
-
-	void readVTOC();
-	void readSectorList(TrackSector start, Common::Array<TrackSector> &list);
-	Common::SeekableReadStream *createReadStreamText(const TOCEntry &entry) const;
-	Common::SeekableReadStream *createReadStreamBinary(const TOCEntry &entry) const;
-
-	DiskImage *_disk;
-	Common::HashMap<Common::Path, TOCEntry, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> _toc;
-};
-
 // On the Apple II, sector headers contain a disk volume number. This number
 // is used by ADL multi-disk games. The PC port has the disk volume number
 // as the first data byte of every sector that contains game data. We need
 // to skip this number as we read in the data. Additionally, the data is now
 // prefixed with an uint16 containing the data size.
-class DataBlock_PC : public DataBlock {
+class DataBlock_PC : public Common::DataBlock {
 public:
-	DataBlock_PC(DiskImage *disk, byte track, byte sector, uint16 offset = 0) :
+	DataBlock_PC(Common::DiskImage *disk, byte track, byte sector, uint16 offset = 0) :
 			_disk(disk),
 			_track(track),
 			_sector(sector),
@@ -192,7 +55,7 @@ public:
 private:
 	void read(Common::SeekableReadStream &stream, byte *const dataPtr, const uint32 size) const;
 
-	DiskImage *_disk;
+	Common::DiskImage *_disk;
 	byte _track, _sector;
 	uint16 _offset;
 };
diff --git a/engines/adl/hires1.cpp b/engines/adl/hires1.cpp
index d3b95b9a6d4..bce9b04b871 100644
--- a/engines/adl/hires1.cpp
+++ b/engines/adl/hires1.cpp
@@ -92,9 +92,9 @@ protected:
 	void showInstructions(Common::SeekableReadStream &stream);
 	void wordWrap(Common::String &str) const;
 
-	Files *_files;
+	Common::Files *_files;
 	Common::File _exe;
-	Common::Array<DataBlockPtr> _corners;
+	Common::Array<Common::DataBlockPtr> _corners;
 	Common::Array<byte> _roomDesc;
 	bool _messageDelay;
 
@@ -147,7 +147,7 @@ void HiRes1Engine::showInstructions(Common::SeekableReadStream &stream) {
 }
 
 void HiRes1Engine::runIntro() {
-	StreamPtr stream(_files->createReadStream(IDS_HR1_EXE_0));
+	Common::StreamPtr stream(_files->createReadStream(IDS_HR1_EXE_0));
 
 	// Early version have no bitmap in 'AUTO LOAD OBJ'
 	if (getGameVersion() >= GAME_VER_HR1_COARSE) {
@@ -180,7 +180,7 @@ void HiRes1Engine::runIntro() {
 		if (!_files->exists(fileName))
 			fileName = "HELLO";
 
-		StreamPtr basic(_files->createReadStream(fileName));
+		Common::StreamPtr basic(_files->createReadStream(fileName));
 
 		_display->setMode(Display::kModeText);
 		_display->home();
@@ -269,9 +269,9 @@ void HiRes1Engine::runIntro() {
 
 void HiRes1Engine::init() {
 	if (Common::File::exists("ADVENTURE")) {
-		_files = new Files_Plain();
+		_files = new Common::Files_Plain();
 	} else {
-		Files_AppleDOS *files = new Files_AppleDOS();
+		Common::Files_AppleDOS *files = new Common::Files_AppleDOS();
 		if (!files->open(getDiskImageName(0)))
 			error("Failed to open '%s'", getDiskImageName(0).toString(Common::Path::kNativeSeparator).c_str());
 		_files = files;
@@ -280,7 +280,7 @@ void HiRes1Engine::init() {
 	_graphics = new GraphicsMan_v1<Display_A2>(*static_cast<Display_A2 *>(_display));
 	_display->moveCursorTo(Common::Point(0, 23)); // DOS 3.3
 
-	StreamPtr stream(_files->createReadStream(IDS_HR1_EXE_1));
+	Common::StreamPtr stream(_files->createReadStream(IDS_HR1_EXE_1));
 
 	Common::StringArray exeStrings;
 	extractExeStrings(*stream, 0x1576, exeStrings);
@@ -363,7 +363,7 @@ void HiRes1Engine::init() {
 void HiRes1Engine::initGameState() {
 	_state.vars.resize(IDI_HR1_NUM_VARS);
 
-	StreamPtr stream(_files->createReadStream(IDS_HR1_EXE_1));
+	Common::StreamPtr stream(_files->createReadStream(IDS_HR1_EXE_1));
 
 	// Load room data from executable
 	_roomDesc.clear();
@@ -423,7 +423,7 @@ void HiRes1Engine::printString(const Common::String &str) {
 
 Common::String HiRes1Engine::loadMessage(uint idx) const {
 	const char returnChar = _display->asciiToNative('\r');
-	StreamPtr stream(_messages[idx]->createReadStream());
+	Common::StreamPtr stream(_messages[idx]->createReadStream());
 	return readString(*stream, returnChar) + returnChar;
 }
 
@@ -493,7 +493,7 @@ void HiRes1Engine::drawItems() {
 
 void HiRes1Engine::drawItem(Item &item, const Common::Point &pos) {
 	if (item.isShape) {
-		StreamPtr stream(_corners[item.picture - 1]->createReadStream());
+		Common::StreamPtr stream(_corners[item.picture - 1]->createReadStream());
 		Common::Point p(pos);
 		_graphics->drawShape(*stream, p);
 	} else
@@ -604,7 +604,7 @@ void HiRes1Engine_VF::getInput(uint &verb, uint &noun) {
 }
 
 void HiRes1Engine_VF::runIntro() {
-	StreamPtr stream(_files->createReadStream(IDS_HR1_EXE_0));
+	Common::StreamPtr stream(_files->createReadStream(IDS_HR1_EXE_0));
 
 	stream->seek(0x1000);
 
diff --git a/engines/adl/hires2.cpp b/engines/adl/hires2.cpp
index e87ff449f9f..c54826db470 100644
--- a/engines/adl/hires2.cpp
+++ b/engines/adl/hires2.cpp
@@ -60,13 +60,13 @@ HiResBaseEngine::HiResBaseEngine(OSystem *syst, const AdlGameDescription *gd, co
 void HiResBaseEngine::init() {
 	_graphics = new GraphicsMan_v2<Display_A2>(*static_cast<Display_A2 *>(_display));
 
-	_disk = new DiskImage();
+	_disk = new Common::DiskImage();
 	if (!_disk->open(getDiskImageName(0)))
 		error("Failed to open disk image '%s'", getDiskImageName(0).toString(Common::Path::kNativeSeparator).c_str());
 
 	_disk->setSectorLimit(13);
 
-	StreamPtr stream(_disk->createReadStream(0x1f, 0x2, 0x00, 4));
+	Common::StreamPtr stream(_disk->createReadStream(0x1f, 0x2, 0x00, 4));
 	loadMessages(*stream, _numMsgs);
 
 	stream.reset(_disk->createReadStream(0x19, 0x0, 0x00, 25, 13));
@@ -114,7 +114,7 @@ void HiResBaseEngine::init() {
 void HiResBaseEngine::initGameState() {
 	_state.vars.resize(40);
 
-	StreamPtr stream(_disk->createReadStream(0x21, 0x5, 0x0e, 7));
+	Common::StreamPtr stream(_disk->createReadStream(0x21, 0x5, 0x0e, 7));
 	loadRooms(*stream, _numRooms);
 
 	stream.reset(_disk->createReadStream(0x21, 0x0, 0x00, 2));
@@ -145,7 +145,7 @@ void HiRes2Engine::runIntro() {
 	if (_disk->getSectorsPerTrack() != 16)
 		return;
 
-	StreamPtr stream(_disk->createReadStream(0x00, 0xd, 0x17, 1));
+	Common::StreamPtr stream(_disk->createReadStream(0x00, 0xd, 0x17, 1));
 
 	_display->setMode(Display::kModeText);
 
diff --git a/engines/adl/hires4.cpp b/engines/adl/hires4.cpp
index a6147f7082e..9cdb0cccbc7 100644
--- a/engines/adl/hires4.cpp
+++ b/engines/adl/hires4.cpp
@@ -59,7 +59,7 @@ protected:
 	void init() override;
 	void initGameState() override;
 
-	DiskImage *_boot;
+	Common::DiskImage *_boot;
 };
 
 HiRes4BaseEngine::HiRes4BaseEngine(OSystem *syst, const AdlGameDescription *gd) :
@@ -81,7 +81,7 @@ HiRes4BaseEngine::~HiRes4BaseEngine() {
 void HiRes4BaseEngine::init() {
 	_graphics = new GraphicsMan_v2<Display_A2>(*static_cast<Display_A2 *>(_display));
 
-	_boot = new DiskImage();
+	_boot = new Common::DiskImage();
 	if (!_boot->open(getDiskImageName(0)))
 		error("Failed to open disk image '%s'", getDiskImageName(0).toString(Common::Path::kNativeSeparator).c_str());
 
@@ -104,7 +104,7 @@ private:
 };
 
 void HiRes4Engine_v1_0::runIntro() {
-	StreamPtr stream(_boot->createReadStream(0x06, 0x3, 0xb9, 1));
+	Common::StreamPtr stream(_boot->createReadStream(0x06, 0x3, 0xb9, 1));
 
 	_display->setMode(Display::kModeText);
 
@@ -121,7 +121,7 @@ void HiRes4Engine_v1_0::runIntro() {
 void HiRes4Engine_v1_0::init() {
 	HiRes4BaseEngine::init();
 
-	StreamPtr stream(_boot->createReadStream(0x9, 0x1, 0x00, 13));
+	Common::StreamPtr stream(_boot->createReadStream(0x9, 0x1, 0x00, 13));
 	Common::StringArray exeStrings;
 	extractExeStrings(*stream, 0x1566, exeStrings);
 	mapExeStrings(exeStrings);
@@ -157,7 +157,7 @@ void HiRes4Engine_v1_0::init() {
 void HiRes4Engine_v1_0::initGameState() {
 	HiRes4BaseEngine::initGameState();
 
-	StreamPtr stream(_boot->createReadStream(0x04, 0xa, 0x0e, 9, 13));
+	Common::StreamPtr stream(_boot->createReadStream(0x04, 0xa, 0x0e, 9, 13));
 	loadRooms(*stream, IDI_HR4_NUM_ROOMS);
 
 	stream.reset(_boot->createReadStream(0x04, 0x5, 0x00, 12, 13));
@@ -177,7 +177,7 @@ private:
 
 // TODO: It might be worth replacing this with a more generic variant that
 // can be used in both hires4 and hires6
-static Common::MemoryReadStream *readSkewedSectors(DiskImage *disk, byte track, byte sector, byte count) {
+static Common::MemoryReadStream *readSkewedSectors(Common::DiskImage *disk, byte track, byte sector, byte count) {
 	const uint bytesPerSector = disk->getBytesPerSector();
 	const uint sectorsPerTrack = disk->getSectorsPerTrack();
 	const uint bufSize = count * bytesPerSector;
@@ -185,7 +185,7 @@ static Common::MemoryReadStream *readSkewedSectors(DiskImage *disk, byte track,
 	byte *p = buf;
 
 	while (count-- != 0) {
-		StreamPtr stream(disk->createReadStream(track, sector));
+		Common::StreamPtr stream(disk->createReadStream(track, sector));
 		stream->read(p, bytesPerSector);
 
 		if (stream->err() || stream->eos())
@@ -222,10 +222,10 @@ static Common::MemoryReadStream *decodeData(Common::SeekableReadStream &stream,
 }
 
 void HiRes4Engine_v1_1::runIntro() {
-	Common::ScopedPtr<Files_AppleDOS> files(new Files_AppleDOS());
+	Common::ScopedPtr<Common::Files_AppleDOS> files(new Common::Files_AppleDOS());
 	files->open(getDiskImageName(0));
 
-	StreamPtr menu(files->createReadStream("\b\b\b\b\b\b\bULYSSES\r(C) 1982"));
+	Common::StreamPtr menu(files->createReadStream("\b\b\b\b\b\b\bULYSSES\r(C) 1982"));
 	menu->seek(0x2eb);
 
 	for (uint i = 0; i < 4; ++i) {
@@ -241,7 +241,7 @@ void HiRes4Engine_v1_1::runIntro() {
 void HiRes4Engine_v1_1::init() {
 	HiRes4BaseEngine::init();
 
-	StreamPtr stream(readSkewedSectors(_boot, 0x05, 0x6, 1));
+	Common::StreamPtr stream(readSkewedSectors(_boot, 0x05, 0x6, 1));
 	_strings.verbError = readStringAt(*stream, 0x4f);
 	_strings.nounError = readStringAt(*stream, 0x8e);
 	_strings.enterCommand = readStringAt(*stream, 0xbc);
@@ -297,7 +297,7 @@ void HiRes4Engine_v1_1::init() {
 void HiRes4Engine_v1_1::initGameState() {
 	HiRes4BaseEngine::initGameState();
 
-	StreamPtr stream(readSkewedSectors(_boot, 0x0b, 0x9, 10));
+	Common::StreamPtr stream(readSkewedSectors(_boot, 0x0b, 0x9, 10));
 	stream->skip(0x0e);
 	loadRooms(*stream, IDI_HR4_NUM_ROOMS);
 
@@ -420,7 +420,7 @@ void HiRes4Engine_LNG::runIntroLogo(Common::SeekableReadStream &ms2) {
 
 void HiRes4Engine_LNG::runIntroTitle(Common::SeekableReadStream &menu, Common::SeekableReadStream &ms2) {
 	ms2.seek(0x2290);
-	StreamPtr shapeTable(ms2.readStream(0x450));
+	Common::StreamPtr shapeTable(ms2.readStream(0x450));
 	if (ms2.err() || ms2.eos())
 		error("Failed to read shape table");
 
@@ -543,17 +543,17 @@ void HiRes4Engine_LNG::runIntroLoading(Common::SeekableReadStream &adventure) {
 }
 
 void HiRes4Engine_LNG::runIntro() {
-	Common::ScopedPtr<Files_AppleDOS> files(new Files_AppleDOS());
+	Common::ScopedPtr<Common::Files_AppleDOS> files(new Common::Files_AppleDOS());
 	files->open(getDiskImageName(0));
 
 	while (!shouldQuit()) {
-		StreamPtr menu(files->createReadStream("MENU"));
+		Common::StreamPtr menu(files->createReadStream("MENU"));
 
 		const bool oldVersion = files->exists("MS2");
 
 		if (oldVersion) {
 			// Version 0.0
-			StreamPtr ms2(files->createReadStream("MS2"));
+			Common::StreamPtr ms2(files->createReadStream("MS2"));
 			runIntroLogo(*ms2);
 
 			if (shouldQuit())
@@ -591,11 +591,11 @@ void HiRes4Engine_LNG::runIntro() {
 				return;
 
 			if (key == _display->asciiToNative('1')) {
-				StreamPtr instructions(files->createReadStream("INSTRUCTIONS"));
+				Common::StreamPtr instructions(files->createReadStream("INSTRUCTIONS"));
 				runIntroInstructions(*instructions);
 				break;
 			} else if (key == _display->asciiToNative('2')) {
-				StreamPtr adventure(files->createReadStream("THE ADVENTURE"));
+				Common::StreamPtr adventure(files->createReadStream("THE ADVENTURE"));
 				runIntroLoading(*adventure);
 				return;
 			}
@@ -622,12 +622,12 @@ private:
 	// AdlEngine_v2
 	void adjustDataBlockPtr(byte &track, byte &sector, byte &offset, byte &size) const override;
 
-	Common::SeekableReadStream *createReadStream(DiskImage *disk, byte track, byte sector, byte offset = 0, byte size = 0) const;
+	Common::SeekableReadStream *createReadStream(Common::DiskImage *disk, byte track, byte sector, byte offset = 0, byte size = 0) const;
 	void loadCommonData();
 	void insertDisk(byte diskNr);
 	void rebindDisk();
 
-	DiskImage *_boot;
+	Common::DiskImage *_boot;
 	byte _curDisk;
 };
 
@@ -640,14 +640,14 @@ HiRes4Engine_Atari::~HiRes4Engine_Atari() {
 void HiRes4Engine_Atari::init() {
 	_graphics = new GraphicsMan_v2<Display_A2>(*static_cast<Display_A2 *>(_display));
 
-	_boot = new DiskImage();
+	_boot = new Common::DiskImage();
 	if (!_boot->open(atariDisks[0]))
 		error("Failed to open disk image '%s'", atariDisks[0]);
 
 	insertDisk(1);
 	loadCommonData();
 
-	StreamPtr stream(createReadStream(_boot, 0x06, 0x2));
+	Common::StreamPtr stream(createReadStream(_boot, 0x06, 0x2));
 	_strings.verbError = readStringAt(*stream, 0x4f);
 	_strings.nounError = readStringAt(*stream, 0x83);
 	_strings.enterCommand = readStringAt(*stream, 0xa6);
@@ -725,18 +725,18 @@ void HiRes4Engine_Atari::insertDisk(byte diskNr) {
 	_curDisk = diskNr;
 
 	delete _disk;
-	_disk = new DiskImage();
+	_disk = new Common::DiskImage();
 	if (!_disk->open(atariDisks[diskNr]))
 		error("Failed to open disk image '%s'", atariDisks[diskNr]);
 }
 
 void HiRes4Engine_Atari::rebindDisk() {
-	// As room.data is bound to the DiskImage, we need to rebind them here
+	// As room.data is bound to the Common::DiskImage, we need to rebind them here
 	// We cannot simply reload the rooms as that would reset their state
 
-	// FIXME: Remove DataBlockPtr-DiskImage coupling?
+	// FIXME: Remove DataBlockPtr-Common::DiskImage coupling?
 
-	StreamPtr stream(createReadStream(_boot, 0x03, 0x1, 0x0e, 9));
+	Common::StreamPtr stream(createReadStream(_boot, 0x03, 0x1, 0x0e, 9));
 	for (uint i = 0; i < IDI_HR4_NUM_ROOMS; ++i) {
 		stream->skip(7);
 		_state.rooms[i].data = readDataBlockPtr(*stream);
@@ -749,7 +749,7 @@ void HiRes4Engine_Atari::rebindDisk() {
 
 void HiRes4Engine_Atari::loadCommonData() {
 	_messages.clear();
-	StreamPtr stream(createReadStream(_boot, 0x0a, 0x4, 0x00, 3));
+	Common::StreamPtr stream(createReadStream(_boot, 0x0a, 0x4, 0x00, 3));
 	loadMessages(*stream, IDI_HR4_NUM_MESSAGES);
 
 	_pictures.clear();
@@ -764,7 +764,7 @@ void HiRes4Engine_Atari::loadCommonData() {
 void HiRes4Engine_Atari::initGameState() {
 	_state.vars.resize(IDI_HR4_NUM_VARS);
 
-	StreamPtr stream(createReadStream(_boot, 0x03, 0x1, 0x0e, 9));
+	Common::StreamPtr stream(createReadStream(_boot, 0x03, 0x1, 0x0e, 9));
 	loadRooms(*stream, IDI_HR4_NUM_ROOMS);
 
 	stream.reset(createReadStream(_boot, 0x02, 0xc, 0x00, 12));
@@ -774,7 +774,7 @@ void HiRes4Engine_Atari::initGameState() {
 	_display->moveCursorTo(Common::Point(0, 23));
 }
 
-Common::SeekableReadStream *HiRes4Engine_Atari::createReadStream(DiskImage *disk, byte track, byte sector, byte offset, byte size) const {
+Common::SeekableReadStream *HiRes4Engine_Atari::createReadStream(Common::DiskImage *disk, byte track, byte sector, byte offset, byte size) const {
 	adjustDataBlockPtr(track, sector, offset, size);
 	return disk->createReadStream(track, sector, offset, size);
 }
diff --git a/engines/adl/hires5.cpp b/engines/adl/hires5.cpp
index 6f60427ced2..8a14b0ab24f 100644
--- a/engines/adl/hires5.cpp
+++ b/engines/adl/hires5.cpp
@@ -243,7 +243,7 @@ void HiRes5Engine::runIntro() {
 
 	insertDisk(2);
 
-	StreamPtr stream(_disk->createReadStream(0x10, 0x0, 0x00, 31));
+	Common::StreamPtr stream(_disk->createReadStream(0x10, 0x0, 0x00, 31));
 
 	display->setMode(Display::kModeGraphics);
 	display->loadFrameBuffer(*stream);
@@ -274,7 +274,7 @@ void HiRes5Engine::init() {
 
 	insertDisk(2);
 
-	StreamPtr stream(_disk->createReadStream(0x5, 0x0, 0x02));
+	Common::StreamPtr stream(_disk->createReadStream(0x5, 0x0, 0x02));
 	loadRegionLocations(*stream, kRegions);
 
 	stream.reset(_disk->createReadStream(0xd, 0x2, 0x04));
@@ -331,7 +331,7 @@ void HiRes5Engine::initGameState() {
 
 	insertDisk(2);
 
-	StreamPtr stream(_disk->createReadStream(0x5, 0x1, 0x00, 3));
+	Common::StreamPtr stream(_disk->createReadStream(0x5, 0x1, 0x00, 3));
 	loadItems(*stream);
 
 	// A combined total of 1213 rooms
diff --git a/engines/adl/hires6.cpp b/engines/adl/hires6.cpp
index 7c623e1e264..5fdb6ad8ed4 100644
--- a/engines/adl/hires6.cpp
+++ b/engines/adl/hires6.cpp
@@ -161,13 +161,13 @@ bool HiRes6Engine::canSaveGameStateCurrently(Common::U32String *msg) {
 #define SECTORS_PER_TRACK 16
 #define BYTES_PER_SECTOR 256
 
-static Common::MemoryReadStream *loadSectors(DiskImage *disk, byte track, byte sector = SECTORS_PER_TRACK - 1, byte count = SECTORS_PER_TRACK) {
+static Common::MemoryReadStream *loadSectors(Common::DiskImage *disk, byte track, byte sector = SECTORS_PER_TRACK - 1, byte count = SECTORS_PER_TRACK) {
 	const int bufSize = count * BYTES_PER_SECTOR;
 	byte *const buf = (byte *)malloc(bufSize);
 	byte *p = buf;
 
 	while (count-- > 0) {
-		StreamPtr stream(disk->createReadStream(track, sector, 0, 0));
+		Common::StreamPtr stream(disk->createReadStream(track, sector, 0, 0));
 		stream->read(p, BYTES_PER_SECTOR);
 
 		if (stream->err() || stream->eos())
@@ -195,7 +195,7 @@ void HiRes6Engine::runIntro() {
 
 	insertDisk(0);
 
-	StreamPtr stream(loadSectors(_disk, 11, 1, 96));
+	Common::StreamPtr stream(loadSectors(_disk, 11, 1, 96));
 
 	display->setMode(Display::kModeGraphics);
 	display->loadFrameBuffer(*stream);
@@ -209,7 +209,7 @@ void HiRes6Engine::runIntro() {
 	display->loadFrameBuffer(*stream);
 
 	// Load copyright string from boot file
-	Files_AppleDOS *files(new Files_AppleDOS());
+	Common::Files_AppleDOS *files(new Common::Files_AppleDOS());
 
 	if (!files->open(getDiskImageName(0)))
 		error("Failed to open disk volume 0");
@@ -232,7 +232,7 @@ void HiRes6Engine::init() {
 
 	insertDisk(0);
 
-	StreamPtr stream(_disk->createReadStream(0x3, 0xf, 0x05));
+	Common::StreamPtr stream(_disk->createReadStream(0x3, 0xf, 0x05));
 	loadRegionLocations(*stream, kRegions);
 
 	stream.reset(_disk->createReadStream(0x5, 0xa, 0x07));
@@ -273,7 +273,7 @@ void HiRes6Engine::initGameState() {
 
 	insertDisk(0);
 
-	StreamPtr stream(_disk->createReadStream(0x3, 0xe, 0x03));
+	Common::StreamPtr stream(_disk->createReadStream(0x3, 0xe, 0x03));
 	loadItems(*stream);
 
 	// A combined total of 91 rooms


Commit: f31eb92d7af8ed1a250a6bcfdbc4fcab6e293bc0
    https://github.com/scummvm/scummvm/commit/f31eb92d7af8ed1a250a6bcfdbc4fcab6e293bc0
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2024-08-25T14:57:06+03:00

Commit Message:
COMMON: Add DiskImage enhancements

- open(FSNode)
- releaseStream()
- ".do" file extension recognized
- Parsing errors handled instead of calling error()
- Skip sector logging when it does nothing

Changed paths:
    common/formats/disk_image.cpp
    common/formats/disk_image.h


diff --git a/common/formats/disk_image.cpp b/common/formats/disk_image.cpp
index f3991c7f5aa..75e936a33f9 100644
--- a/common/formats/disk_image.cpp
+++ b/common/formats/disk_image.cpp
@@ -239,6 +239,10 @@ static bool decodeTrack(Common::SeekableReadStream &stream, uint trackLen, bool
 }
 
 static void printGoodSectors(Common::Array<bool> &goodSectors, uint sectorsPerTrack) {
+	if (gDebugLevel < 1) {
+		return;
+	}
+
 	if (Common::find(goodSectors.begin(), goodSectors.end(), false) != goodSectors.end()) {
 		debugN(1, "NIB: Bad/missing sectors:");
 
@@ -405,8 +409,11 @@ static Common::SeekableReadStream *readImage_WOZ(Common::File &f, bool dos33, ui
 		StreamPtr stream(readTrack_WOZ(f, track, version == 2));
 
 		if (stream) {
-			if (!decodeTrack(*stream, stream->size(), dos33, diskImage, tracks, goodSectors))
-				error("WOZ: error reading '%s'", f.getName());
+			if (!decodeTrack(*stream, stream->size(), dos33, diskImage, tracks, goodSectors)) {
+				warning("WOZ: error reading '%s'", f.getName());
+				free(diskImage);
+				return nullptr;
+			}
 		}
 	}
 
@@ -416,30 +423,46 @@ static Common::SeekableReadStream *readImage_WOZ(Common::File &f, bool dos33, ui
 }
 
 bool DiskImage::open(const Common::Path &filename) {
-	Common::File *f = new Common::File;
-
+	Common::File *file = new Common::File();
 	debug(1, "Opening '%s'", filename.toString(Common::Path::kNativeSeparator).c_str());
-
-	if (!f->open(filename)) {
+	if (file->open(filename)) {
+		if (open(filename.baseName(), file)) {
+			return true;
+		}
+	} else {
 		warning("Failed to open '%s'", filename.toString(Common::Path::kNativeSeparator).c_str());
-		delete f;
-		return false;
 	}
+	delete file;
+	return false;
+}
 
-	Common::String lcName(filename.baseName());
-	lcName.toLowercase();
+bool DiskImage::open(const Common::FSNode &node) {
+	Common::File *file = new Common::File();
+	debug(1, "Opening '%s'", node.getPath().toString(Common::Path::kNativeSeparator).c_str());
+	if (file->open(node)) {
+		if (open(node.getFileName(), file)) {
+			return true;
+		}
+	} else {
+		warning("Failed to open '%s'", node.getPath().toString(Common::Path::kNativeSeparator).c_str());
+	}
+	delete file;
+	return false;
+}
 
-	if (lcName.hasSuffix(".dsk")) {
+bool DiskImage::open(const Common::String &name, Common::File *f) {
+	if (name.hasSuffixIgnoreCase(".dsk") ||
+		name.hasSuffixIgnoreCase(".do")) {
 		_tracks = 35;
 		_sectorsPerTrack = 16;
 		_bytesPerSector = 256;
 		_stream = f;
-	} else if (lcName.hasSuffix(".d13")) {
+	} else if (name.hasSuffixIgnoreCase(".d13")) {
 		_tracks = 35;
 		_sectorsPerTrack = 13;
 		_bytesPerSector = 256;
 		_stream = f;
-	} else if (lcName.hasSuffix(".nib")) {
+	} else if (name.hasSuffixIgnoreCase(".nib")) {
 		_tracks = 35;
 
 		if (detectDOS33(*f, kNibTrackLen))
@@ -450,8 +473,7 @@ bool DiskImage::open(const Common::Path &filename) {
 		_bytesPerSector = 256;
 		f->seek(0);
 		_stream = readImage_NIB(*f, _sectorsPerTrack == 16);
-		delete f;
-	} else if (lcName.hasSuffix(".woz")) {
+	} else if (name.hasSuffixIgnoreCase(".woz")) {
 		_tracks = 35;
 		_sectorsPerTrack = 13;
 		_bytesPerSector = 256;
@@ -468,14 +490,12 @@ bool DiskImage::open(const Common::Path &filename) {
 				warning("WOZ: failed to load bitstream for track 0 in '%s'", f->getName());
 			}
 		}
-
-		delete f;
-	} else if (lcName.hasSuffix(".xfd")) {
+	} else if (name.hasSuffixIgnoreCase(".xfd")) {
 		_tracks = 40;
 		_sectorsPerTrack = 18;
 		_bytesPerSector = 128;
 		_stream = f;
-	} else if (lcName.hasSuffix(".img")) {
+	} else if (name.hasSuffixIgnoreCase(".img")) {
 		_tracks = 40;
 		_sectorsPerTrack = 8;
 		_bytesPerSector = 512;
@@ -485,12 +505,22 @@ bool DiskImage::open(const Common::Path &filename) {
 
 	int expectedSize = _tracks * _sectorsPerTrack * _bytesPerSector;
 
-	if (!_stream)
+	if (_stream == nullptr) {
 		return false;
+	}
 
-	if (_stream->size() != expectedSize)
-		error("Unrecognized disk image '%s' of size %d bytes (expected %d bytes)", filename.toString(Common::Path::kNativeSeparator).c_str(), (int)_stream->size(), expectedSize);
+	if (_stream->size() != expectedSize) {
+		warning("Unrecognized disk image '%s' of size %d bytes (expected %d bytes)", f->getName(), (int)_stream->size(), expectedSize);
+		if (_stream != f) {
+			delete _stream;
+			_stream = nullptr;
+		}
+		return false;
+	};
 
+	if (_stream != f) {
+		delete f;
+	}
 	return true;
 }
 
@@ -506,8 +536,11 @@ Common::SeekableReadStream *DiskImage::createReadStream(uint track, uint sector,
 	if (sectorLimit == 0)
 		sectorLimit = _sectorsPerTrack;
 
-	if (sector < _firstSector || sector >= sectorLimit + _firstSector)
-		error("Sector %u is out of bounds for %u-sector %u-based reading", sector, sectorLimit, _firstSector);
+	if (sector < _firstSector || sector >= sectorLimit + _firstSector) {
+		warning("Sector %u is out of bounds for %u-sector %u-based reading", sector, sectorLimit, _firstSector);
+		free(data);
+		return nullptr;
+	}
 
 	sector -= _firstSector;
 
@@ -518,8 +551,11 @@ Common::SeekableReadStream *DiskImage::createReadStream(uint track, uint sector,
 		if (bytesToRead - dataOffset < bytesRemInTrack)
 			bytesRemInTrack = bytesToRead - dataOffset;
 
-		if (_stream->read(data + dataOffset, bytesRemInTrack) < bytesRemInTrack)
-			error("Error reading disk image at track %d; sector %d", track, sector);
+		if (_stream->read(data + dataOffset, bytesRemInTrack) < bytesRemInTrack) {
+			warning("Error reading disk image at track %d; sector %d", track, sector);
+			free(data);
+			return nullptr;
+		}
 
 		++track;
 
@@ -532,6 +568,20 @@ Common::SeekableReadStream *DiskImage::createReadStream(uint track, uint sector,
 	return new Common::MemoryReadStream(data, bytesToRead, DisposeAfterUse::YES);
 }
 
+Common::SeekableReadStream *DiskImage::releaseStream() {
+	Common::SeekableReadStream *stream = _stream;
+	
+	// reset class
+	_stream = nullptr;
+	_tracks = 0;
+	_sectorsPerTrack = 0;
+	_bytesPerSector = 0;
+	_sectorLimit = 0;
+	_firstSector = 0;
+
+	return stream;
+}
+
 int32 computeMD5(const Common::FSNode &node, Common::String &md5, uint32 md5Bytes) {
 	Common::File f;
 
diff --git a/common/formats/disk_image.h b/common/formats/disk_image.h
index ee7ee6d8477..d0a3805ab7a 100644
--- a/common/formats/disk_image.h
+++ b/common/formats/disk_image.h
@@ -86,8 +86,10 @@ public:
 	}
 
 	bool open(const Common::Path &filename);
+	bool open(const Common::FSNode &node);
 	const DataBlockPtr getDataBlock(uint track, uint sector, uint offset = 0, uint size = 0) const;
 	Common::SeekableReadStream *createReadStream(uint track, uint sector, uint offset = 0, uint size = 0, uint sectorsUsed = 0) const;
+	Common::SeekableReadStream *releaseStream();
 	void setSectorLimit(uint sectorLimit) { _sectorLimit = sectorLimit; } // Maximum number of sectors to read per track before stepping
 	uint getBytesPerSector() const { return _bytesPerSector; }
 	uint getSectorsPerTrack() const { return _sectorsPerTrack; }
@@ -117,6 +119,9 @@ protected:
 	Common::SeekableReadStream *_stream;
 	uint _tracks, _sectorsPerTrack, _bytesPerSector, _firstSector;
 	uint _sectorLimit;
+	
+private:
+	bool open(const Common::String &name, Common::File *f);
 };
 
 // Data in plain files


Commit: 5a7555dfccff5b26ad0c0f8bc9508d426b36dff5
    https://github.com/scummvm/scummvm/commit/5a7555dfccff5b26ad0c0f8bc9508d426b36dff5
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2024-08-25T14:57:06+03:00

Commit Message:
COMMON: Add BitArray::size()

Changed paths:
    common/bitarray.h


diff --git a/common/bitarray.h b/common/bitarray.h
index bfb89823687..2e69322c78b 100644
--- a/common/bitarray.h
+++ b/common/bitarray.h
@@ -68,6 +68,9 @@ public:
 		return _bits[bit / 8] & (1 << (bit % 8));
 	}
 
+	uint size() const {
+		return _bitcount;
+	}
 private:
 	uint _bitcount;
 	byte *_bits;


Commit: 23a9f4ed541e6e7db241c81f09dd636c7c8a5135
    https://github.com/scummvm/scummvm/commit/23a9f4ed541e6e7db241c81f09dd636c7c8a5135
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2024-08-25T14:57:06+03:00

Commit Message:
COMMON: Add DiskImage lazy-decoding mode

Images can now be read without decoding all disk tracks.

This allows DiskImage to be used in detection code, where only
a little disk data is needed. Otherwise, formats such as WOZ
would cause noticeable delays due to decoding entire disks.

Changed paths:
    common/formats/disk_image.cpp
    common/formats/disk_image.h


diff --git a/common/formats/disk_image.cpp b/common/formats/disk_image.cpp
index 75e936a33f9..9d77213d633 100644
--- a/common/formats/disk_image.cpp
+++ b/common/formats/disk_image.cpp
@@ -106,7 +106,7 @@ static uint8 read44(byte *buffer, uint size, uint &pos) {
 	return ((ret << 1) | 1) & buffer[pos++ % size];
 }
 
-static bool decodeTrack(Common::SeekableReadStream &stream, uint trackLen, bool dos33, byte *const diskImage, uint tracks, Common::Array<bool> &goodSectors) {
+static bool decodeTrack(Common::SeekableReadStream &stream, uint trackLen, bool dos33, byte *const diskImage, uint tracks, Common::BitArray &goodSectors) {
 	// starting at 0xaa, 64 is invalid (see below)
 	const byte c_5and3_lookup[] = { 64, 0, 64, 1, 2, 3, 64, 64, 64, 64, 64, 4, 5, 6, 64, 64, 7, 8, 64, 9, 10, 11, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 12, 13, 64, 64, 14, 15, 64, 16, 17, 18, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 19, 20, 64, 21, 22, 23, 64, 64, 64, 64, 64, 24, 25, 26, 64, 64, 27, 28, 64, 29, 30, 31 };
 	// starting at 0x96, 64 is invalid (see below)
@@ -231,60 +231,82 @@ static bool decodeTrack(Common::SeekableReadStream &stream, uint trackLen, bool
 			output[255] = (inbuffer[409] << 3) | (inbuffer[0] & 0x7);
 		}
 
-		goodSectors[track * sectorsPerTrack + sector] = true;
+		goodSectors.set(track * sectorsPerTrack + sector);
 	}
 
 	free(buffer);
 	return true;
 }
 
-static void printGoodSectors(Common::Array<bool> &goodSectors, uint sectorsPerTrack) {
+static void printGoodSectors(const Common::BitArray &goodSectors, uint sectorsPerTrack) {
 	if (gDebugLevel < 1) {
 		return;
 	}
 
-	if (Common::find(goodSectors.begin(), goodSectors.end(), false) != goodSectors.end()) {
-		debugN(1, "NIB: Bad/missing sectors:");
-
-		for (uint i = 0; i < goodSectors.size(); ++i) {
-			if (!goodSectors[i])
-				debugN(1, " (%d, %d)", i / sectorsPerTrack, i % sectorsPerTrack);
+	bool foundBadSector = false;
+	for (uint i = 0; i < goodSectors.size(); ++i) {
+		if (!goodSectors.get(i)) {
+			if (!foundBadSector) {
+				debugN(1, "Bad/missing sectors:");
+				foundBadSector = true;
+			}
+			debugN(1, " (%d, %d)", i / sectorsPerTrack, i % sectorsPerTrack);
 		}
-
+	}
+	if (foundBadSector) {
 		debugN(1, "\n");
 	}
 }
 
-static Common::SeekableReadStream *readImage_NIB(Common::File &f, bool dos33, uint tracks = 35) {
+static bool readImage_NIB(
+	const char *filename,
+	Common::SeekableReadStream &f,
+	byte *diskImage,
+	bool dos33,
+	uint startTrack = 0,
+	uint tracksToRead = 35) {
+
 	if (f.size() != 35 * kNibTrackLen) {
-		warning("NIB: image '%s' has invalid size of %d bytes", f.getName(), (int)f.size());
-		return nullptr;
+		warning("NIB: image '%s' has invalid size of %d bytes", filename, (int)f.size());
+		return false;
 	}
 
 	const uint sectorsPerTrack = (dos33 ? 16 : 13);
-	const uint imageSize = tracks * sectorsPerTrack * 256;
-	byte *const diskImage = (byte *)calloc(imageSize, 1);
-	Common::Array<bool> goodSectors(tracks * sectorsPerTrack);
+	Common::BitArray goodSectors(35 * sectorsPerTrack);
 
-	for (uint track = 0; track < tracks; ++track) {
-		if (!decodeTrack(f, kNibTrackLen, dos33, diskImage, tracks, goodSectors)) {
-			warning("NIB: error reading '%s'", f.getName());
-			free(diskImage);
-			return nullptr;
+	f.seek(startTrack * kNibTrackLen);
+	uint endTrack = startTrack + tracksToRead - 1;
+	for (uint track = startTrack; track <= endTrack; ++track) {
+		if (!decodeTrack(f, kNibTrackLen, dos33, diskImage, 35, goodSectors)) {
+			warning("NIB: error decoding track %d in '%s'", track, filename);
+			return false;
 		}
 	}
 
 	printGoodSectors(goodSectors, sectorsPerTrack);
+	return true;
+}
+
+static Common::SeekableReadStream *readImageToStream_NIB(Common::File &f, bool dos33, uint tracks = 35) {
+	const uint sectorsPerTrack = (dos33 ? 16 : 13);
+	const uint imageSize = tracks * sectorsPerTrack * 256;
+	byte *const diskImage = (byte *)calloc(imageSize, 1);
+
+	if (!readImage_NIB(f.getName(), f, diskImage, dos33, 0, tracks)) {
+		warning("NIB: error reading '%s'", f.getName());
+		free(diskImage);
+		return nullptr;
+	}
 
 	return new Common::MemoryReadStream(diskImage, imageSize, DisposeAfterUse::YES);
 }
 
-static int getVersion_WOZ(Common::File &f) {
+static int getVersion_WOZ(const char *filename, Common::SeekableReadStream &f) {
 	f.seek(0);
 	const uint32 fileId = f.readUint32BE();
 
 	if (f.eos() || f.err()) {
-		warning("WOZ: error reading '%s'", f.getName());
+		warning("WOZ: error reading '%s'", filename);
 		return 0;
 	}
 
@@ -293,16 +315,16 @@ static int getVersion_WOZ(Common::File &f) {
 	else if (fileId == MKTAG('W', 'O', 'Z', '2'))
 		return 2;
 
-	warning("WOZ: unsupported ID '%s' found in '%s'", tag2str(fileId), f.getName());
+	warning("WOZ: unsupported ID '%s' found in '%s'", tag2str(fileId), filename);
 	return 0;
 }
 
-static Common::SeekableReadStream *readTrack_WOZ(Common::File &f, uint track, bool woz2) {
+static Common::SeekableReadStream *readTrack_WOZ(const char *filename, Common::SeekableReadStream &f, uint track, bool woz2) {
 	f.seek(88 + track * 4);
 	const byte index = f.readByte();
 
 	if (index == 0xff) {
-		warning("WOZ: track %u not found in '%s', skipping", track, f.getName());
+		warning("WOZ: track %u not found in '%s', skipping", track, filename);
 		return nullptr;
 	}
 
@@ -323,7 +345,7 @@ static Common::SeekableReadStream *readTrack_WOZ(Common::File &f, uint track, bo
 	f.seek(offset);
 
 	if (f.eos() || f.err() || byteSize == 0) {
-		warning("WOZ: failed to read track %u in '%s', aborting", track, f.getName());
+		warning("WOZ: failed to read track %u in '%s', aborting", track, filename);
 		return nullptr;
 	}
 
@@ -331,14 +353,14 @@ static Common::SeekableReadStream *readTrack_WOZ(Common::File &f, uint track, bo
 	byte *outBuf = (byte *)malloc(byteSize);
 	uint32 outSize = 0;
 	if (!inBuf || !outBuf) {
-		warning("WOZ: failed to create buffers of size %u for track %u in '%s'", byteSize, track, f.getName());
+		warning("WOZ: failed to create buffers of size %u for track %u in '%s'", byteSize, track, filename);
 		free(inBuf);
 		free(outBuf);
 		return nullptr;
 	}
 
 	if (f.read(inBuf, byteSize) < byteSize) {
-		warning("WOZ: error reading track %u in '%s'", track, f.getName());
+		warning("WOZ: error reading track %u in '%s'", track, filename);
 		free(inBuf);
 		free(outBuf);
 		return nullptr;
@@ -360,7 +382,7 @@ static Common::SeekableReadStream *readTrack_WOZ(Common::File &f, uint track, bo
 		if (bitStream.pos() == bitSize) {
 			bitStream.rewind();
 			if (stop) {
-				warning("WOZ: failed to find sync point for track %u in '%s'", track, f.getName());
+				warning("WOZ: failed to find sync point for track %u in '%s'", track, filename);
 				break;
 			}
 			stop = true;
@@ -383,10 +405,10 @@ static Common::SeekableReadStream *readTrack_WOZ(Common::File &f, uint track, bo
 	} while (bitsRead < bitSize);
 
 	if (nibble != 0)
-		warning("WOZ: failed to sync track %u in '%s'", track, f.getName());
+		warning("WOZ: failed to sync track %u in '%s'", track, filename);
 
 	if (outSize == 0) {
-		warning("WOZ: track %u in '%s' is empty", track, f.getName());
+		warning("WOZ: track %u in '%s' is empty", track, filename);
 		free(outBuf);
 		return nullptr;
 	}
@@ -394,30 +416,48 @@ static Common::SeekableReadStream *readTrack_WOZ(Common::File &f, uint track, bo
 	return new Common::MemoryReadStream(outBuf, outSize, DisposeAfterUse::YES);
 }
 
-static Common::SeekableReadStream *readImage_WOZ(Common::File &f, bool dos33, uint tracks = 35) {
-	int version = getVersion_WOZ(f);
+static bool readImage_WOZ(
+	const char *filename,
+	Common::SeekableReadStream &f,
+	byte *diskImage,
+	bool dos33,
+	uint startTrack = 0,
+	uint tracksToRead = 35) {
 
-	if (version == 0)
-		return nullptr;
+	int version = getVersion_WOZ(filename, f);
+	if (version == 0) {
+		return false;
+	}
 
 	const uint sectorsPerTrack = (dos33 ? 16 : 13);
-	const uint imageSize = tracks * sectorsPerTrack * 256;
-	byte *const diskImage = (byte *)calloc(imageSize, 1);
-	Common::Array<bool> goodSectors(tracks * sectorsPerTrack);
+	Common::BitArray goodSectors(35 * sectorsPerTrack);
 
-	for (uint track = 0; track < tracks; ++track) {
-		StreamPtr stream(readTrack_WOZ(f, track, version == 2));
+	uint endTrack = startTrack + tracksToRead - 1;
+	for (uint track = startTrack; track <= endTrack; ++track) {
+		StreamPtr stream(readTrack_WOZ(filename, f, track, version == 2));
 
 		if (stream) {
-			if (!decodeTrack(*stream, stream->size(), dos33, diskImage, tracks, goodSectors)) {
-				warning("WOZ: error reading '%s'", f.getName());
-				free(diskImage);
-				return nullptr;
+			if (!decodeTrack(*stream, stream->size(), dos33, diskImage, 35, goodSectors)) {
+				warning("WOZ: error decoding track %d in '%s'", track, filename);
+				return false;
 			}
 		}
 	}
 
 	printGoodSectors(goodSectors, sectorsPerTrack);
+	return true;
+}
+
+static Common::SeekableReadStream *readImageToStream_WOZ(Common::File &f, bool dos33, uint tracks = 35) {
+	const uint sectorsPerTrack = (dos33 ? 16 : 13);
+	const uint imageSize = tracks * sectorsPerTrack * 256;
+	byte *const diskImage = (byte *)calloc(imageSize, 1);
+
+	if (!readImage_WOZ(f.getName(), f, diskImage, dos33, 0, tracks)) {
+		warning("WOZ: error reading '%s'", f.getName());
+		free(diskImage);
+		return nullptr;
+	}
 
 	return new Common::MemoryReadStream(diskImage, imageSize, DisposeAfterUse::YES);
 }
@@ -456,12 +496,12 @@ bool DiskImage::open(const Common::String &name, Common::File *f) {
 		_tracks = 35;
 		_sectorsPerTrack = 16;
 		_bytesPerSector = 256;
-		_stream = f;
+		_inputStream = f;
 	} else if (name.hasSuffixIgnoreCase(".d13")) {
 		_tracks = 35;
 		_sectorsPerTrack = 13;
 		_bytesPerSector = 256;
-		_stream = f;
+		_inputStream = f;
 	} else if (name.hasSuffixIgnoreCase(".nib")) {
 		_tracks = 35;
 
@@ -471,56 +511,83 @@ bool DiskImage::open(const Common::String &name, Common::File *f) {
 			_sectorsPerTrack = 13;
 
 		_bytesPerSector = 256;
-		f->seek(0);
-		_stream = readImage_NIB(*f, _sectorsPerTrack == 16);
+		if (_lazyDecoding) {
+			// store the file stream and create an empty decode stream.
+			// tracks will be decoded into the decode stream as they're read.
+			uint32 imageSize = _tracks * _sectorsPerTrack * _bytesPerSector;
+			_inputStream = f;
+			_decodeBuffer = (byte *)calloc(imageSize, 1);
+			_decodeStream = new Common::MemoryReadStream(_decodeBuffer, imageSize, DisposeAfterUse::YES);
+			_decodedTracks.set_size(_tracks);
+			_encoding = DiskImageEncodingNib;
+		} else {
+			// decode the entire image
+			_inputStream = readImageToStream_NIB(*f, (_sectorsPerTrack == 16));
+		}
 	} else if (name.hasSuffixIgnoreCase(".woz")) {
 		_tracks = 35;
 		_sectorsPerTrack = 13;
 		_bytesPerSector = 256;
 
-		int version = getVersion_WOZ(*f);
+		int version = getVersion_WOZ(name.c_str(), *f);
 
 		if (version > 0) {
-			StreamPtr bitStream(readTrack_WOZ(*f, 0, version == 2));
+			StreamPtr bitStream(readTrack_WOZ(name.c_str(), *f, 0, version == 2));
 			if (bitStream) {
 				if (detectDOS33(*bitStream, bitStream->size()))
 					_sectorsPerTrack = 16;
-				_stream = readImage_WOZ(*f, _sectorsPerTrack == 16);
+
+				if (_lazyDecoding) {
+					// store the file stream and create an empty decode stream.
+					// tracks will be decoded into the decode stream as they're read.
+					uint32 imageSize = _tracks * _sectorsPerTrack * _bytesPerSector;
+					_inputStream = f;
+					_decodeBuffer = (byte *)calloc(imageSize, 1);
+					_decodeStream = new Common::MemoryReadStream(_decodeBuffer, imageSize, DisposeAfterUse::YES);
+					_decodedTracks.set_size(_tracks);
+					_encoding = DiskImageEncodingWoz;
+				} else {
+					// decode the entire image
+					_inputStream = readImageToStream_WOZ(*f, (_sectorsPerTrack == 16), _tracks);
+				}
 			} else {
-				warning("WOZ: failed to load bitstream for track 0 in '%s'", f->getName());
+				warning("WOZ: failed to load bitstream for track 0 in '%s'", name.c_str());
 			}
 		}
 	} else if (name.hasSuffixIgnoreCase(".xfd")) {
 		_tracks = 40;
 		_sectorsPerTrack = 18;
 		_bytesPerSector = 128;
-		_stream = f;
+		_inputStream = f;
 	} else if (name.hasSuffixIgnoreCase(".img")) {
 		_tracks = 40;
 		_sectorsPerTrack = 8;
 		_bytesPerSector = 512;
 		_firstSector = 1;
-		_stream = f;
+		_inputStream = f;
 	}
-
-	int expectedSize = _tracks * _sectorsPerTrack * _bytesPerSector;
-
-	if (_stream == nullptr) {
+	
+	if (_inputStream == nullptr) {
 		return false;
 	}
 
-	if (_stream->size() != expectedSize) {
-		warning("Unrecognized disk image '%s' of size %d bytes (expected %d bytes)", f->getName(), (int)_stream->size(), expectedSize);
-		if (_stream != f) {
-			delete _stream;
-			_stream = nullptr;
+	int expectedSize = _tracks * _sectorsPerTrack * _bytesPerSector;
+	if (getDiskStream()->size() != expectedSize) {
+		warning("Unrecognized disk image '%s' of size %d bytes (expected %d bytes)", name.c_str(), (int)getDiskStream()->size(), expectedSize);
+		if (_inputStream != f) {
+			delete _inputStream;
+			_inputStream = nullptr;
 		}
+		delete _decodeStream;
+		_decodeStream = nullptr;
 		return false;
 	};
 
-	if (_stream != f) {
+	if (_inputStream != f) {
 		delete f;
 	}
+
+	_name = name;
 	return true;
 }
 
@@ -542,16 +609,32 @@ Common::SeekableReadStream *DiskImage::createReadStream(uint track, uint sector,
 		return nullptr;
 	}
 
-	sector -= _firstSector;
+	// lazy decoding not supported for createReadStream(), because decoding
+	// tracks here requires removing way too many existing const keywords.
+	// if it's ever needed, enable this code and remove all those consts.
+#if 0
+	if (_decodeBuffer != nullptr) {
+		// lazy decoding
+		uint32 bytesPerTrack = _sectorsPerTrack * _bytesPerSector;
+		uint32 endTrack = track + (((sector - _firstSector) * _bytesPerSector + offset + bytesToRead - 1) / bytesPerTrack);
+		for (uint32 t = track; t <= endTrack; t++) {
+			if (!_decodedTracks.get(t)) {
+				decodeTrack(t);
+			}
+		}
+	}
+#endif
 
+	sector -= _firstSector;
+	Common::SeekableReadStream *stream = getDiskStream();
 	while (dataOffset < bytesToRead) {
 		uint bytesRemInTrack = (sectorLimit - 1 - sector) * _bytesPerSector + _bytesPerSector - offset;
-		_stream->seek((track * _sectorsPerTrack + sector) * _bytesPerSector + offset);
+		stream->seek((track * _sectorsPerTrack + sector) * _bytesPerSector + offset);
 
 		if (bytesToRead - dataOffset < bytesRemInTrack)
 			bytesRemInTrack = bytesToRead - dataOffset;
 
-		if (_stream->read(data + dataOffset, bytesRemInTrack) < bytesRemInTrack) {
+		if (stream->read(data + dataOffset, bytesRemInTrack) < bytesRemInTrack) {
 			warning("Error reading disk image at track %d; sector %d", track, sector);
 			free(data);
 			return nullptr;
@@ -569,10 +652,19 @@ Common::SeekableReadStream *DiskImage::createReadStream(uint track, uint sector,
 }
 
 Common::SeekableReadStream *DiskImage::releaseStream() {
-	Common::SeekableReadStream *stream = _stream;
+	Common::SeekableReadStream *stream = getDiskStream();
 	
 	// reset class
-	_stream = nullptr;
+	if (stream != _inputStream) {
+		delete _inputStream;
+	}
+	_inputStream = nullptr;
+	_decodeStream = nullptr;
+	_decodeBuffer = nullptr;
+	_decodedTracks.clear();
+	_encoding = DiskImageEncodingNone;
+	_lazyDecoding = false;
+
 	_tracks = 0;
 	_sectorsPerTrack = 0;
 	_bytesPerSector = 0;
@@ -582,6 +674,42 @@ Common::SeekableReadStream *DiskImage::releaseStream() {
 	return stream;
 }
 
+uint32 DiskImage::read(void *dataPtr, uint32 diskPosition, uint32 dataSize) {
+	Common::SeekableReadStream *stream;
+	if (_decodeBuffer != nullptr) {
+		// lazy decoding
+		uint32 bytesPerTrack = _sectorsPerTrack * _bytesPerSector;
+		uint32 startTrack = diskPosition / bytesPerTrack;
+		uint32 endTrack = (diskPosition + dataSize - 1) / bytesPerTrack;
+		for (uint32 t = startTrack; t <= endTrack; t++) {
+			if (!_decodedTracks.get(t)) {
+				decodeTrack(t);
+			}
+		}
+		stream = _decodeStream;
+	}
+	else {
+		stream = _inputStream;
+	}
+	stream->seek(diskPosition);
+	return stream->read(dataPtr, dataSize);
+}
+
+void DiskImage::decodeTrack(uint track) {
+	switch (_encoding) {
+	case DiskImageEncodingNib:
+		readImage_NIB(_name.c_str(), *_inputStream, _decodeBuffer, (_sectorsPerTrack == 16), track, 1);
+		break;
+	case DiskImageEncodingWoz:
+		readImage_WOZ(_name.c_str(), *_inputStream, _decodeBuffer, (_sectorsPerTrack == 16), track, 1);
+		break;
+	default:
+		return;
+	}
+
+	_decodedTracks.set(track);
+}
+
 int32 computeMD5(const Common::FSNode &node, Common::String &md5, uint32 md5Bytes) {
 	Common::File f;
 
@@ -593,8 +721,7 @@ int32 computeMD5(const Common::FSNode &node, Common::String &md5, uint32 md5Byte
 	if (node.getName().matchString("*.nib", true) && f.size() == 35 * kNibTrackLen) {
 		bool isDOS33 = detectDOS33(f, kNibTrackLen);
 
-		f.seek(0);
-		Common::SeekableReadStream *stream = readImage_NIB(f, isDOS33, tracks);
+		Common::SeekableReadStream *stream = readImageToStream_NIB(f, isDOS33, tracks);
 		if (stream) {
 			md5 = Common::computeStreamMD5AsString(*stream, md5Bytes);
 			delete stream;
@@ -603,13 +730,13 @@ int32 computeMD5(const Common::FSNode &node, Common::String &md5, uint32 md5Byte
 
 		return -1;
 	} else if (node.getName().matchString("*.woz", true)) {
-		int version = getVersion_WOZ(f);
+		int version = getVersion_WOZ(f.getName(), f);
 
 		if (version > 0) {
-			StreamPtr nibbles(readTrack_WOZ(f, 0, version == 2));
+			StreamPtr nibbles(readTrack_WOZ(f.getName(), f, 0, version == 2));
 			if (nibbles) {
 				bool isDOS33 = detectDOS33(*nibbles, nibbles->size());
-				StreamPtr stream(readImage_WOZ(f, isDOS33, tracks));
+				StreamPtr stream(readImageToStream_WOZ(f, isDOS33, tracks));
 				if (stream) {
 					md5 = Common::computeStreamMD5AsString(*stream, md5Bytes);
 					return 35 * (isDOS33 ? 16 : 13) * 256;
diff --git a/common/formats/disk_image.h b/common/formats/disk_image.h
index d0a3805ab7a..5c590cccaba 100644
--- a/common/formats/disk_image.h
+++ b/common/formats/disk_image.h
@@ -19,6 +19,7 @@
  *
  */
 
+#include "common/bitarray.h"
 #include "common/ptr.h"
 #include "common/file.h"
 #include "common/debug.h"
@@ -28,9 +29,44 @@
 
 namespace Common {
 
+// Disk image parsers / decoders
+//
+// These classes handle floppy disk image files. Multiple formats are supported.
+// An image's file name extension determines its format. DiskImage::open selects
+// the format and expected disk size. Data can be read by track/sector/offset
+// or by the calculated stream position. Several file systems can also be read.
+//
+// Supported image formats:
+//  .do   Apple II disk sectors. 35 tracks, 16 sectors, no encoding.
+//  .dsk  Same as .do. Note that alternative sector orders are not handled.
+//        Currently, if clients want to support images with a different sector
+//        order, then they will have to detect and handle it themselves.
+//  .d13  Apple II disk sectors. 35 tracks, 13 sectors, no encoding.
+//  .nib  Apple II disk nibbles. 35 tracks, 13 or 16 sectors, nibble-encoded.
+//  .woz  Apple II comprehensive disk bitstream. 35 tracks, 13 or 16 sectors.
+//        This encoding format takes a noticeable amount of time to decode.
+//  .img  PC disk sectors. 40 tracks, 8 sectors, no encoding.
+//  .xfd  Atari disk sectors. 40 tracks, 18 sectors, no encoding.
+//
+// For encoded formats, the default behavior is to decode every track when
+// opening an image. Lazy decoding can also be enabled, causing tracks to be
+// decoded when they are first accessed. This can significantly speed up access
+// if the format is expensive to decode but only a little data is needed.
+//
+// This code was originally part of the ADL engine.
+
 class SeekableReadStream;
 class String;
 
+/**
+ * Disk image formats that require decoding to read their sectors
+ */
+enum DiskImageEncoding {
+	DiskImageEncodingNone,
+	DiskImageEncodingNib,
+	DiskImageEncodingWoz
+};
+
 // Used for disk image detection
 int32 computeMD5(const Common::FSNode &node, Common::String &md5, uint32 md5Bytes);
 
@@ -74,7 +110,11 @@ protected:
 class DiskImage {
 public:
 	DiskImage() :
-			_stream(nullptr),
+			_inputStream(nullptr),
+			_decodeStream(nullptr),
+			_decodeBuffer(nullptr),
+			_encoding(DiskImageEncodingNone),
+			_lazyDecoding(false),
 			_tracks(0),
 			_sectorsPerTrack(0),
 			_bytesPerSector(0),
@@ -82,7 +122,8 @@ public:
 			_firstSector(0) { }
 
 	~DiskImage() {
-		delete _stream;
+		delete _inputStream;
+		delete _decodeStream; // frees _decodeBuffer
 	}
 
 	bool open(const Common::Path &filename);
@@ -90,6 +131,9 @@ public:
 	const DataBlockPtr getDataBlock(uint track, uint sector, uint offset = 0, uint size = 0) const;
 	Common::SeekableReadStream *createReadStream(uint track, uint sector, uint offset = 0, uint size = 0, uint sectorsUsed = 0) const;
 	Common::SeekableReadStream *releaseStream();
+	Common::SeekableReadStream *getDiskStream() const { return _decodeBuffer ? _decodeStream : _inputStream; }
+	uint32 read(void *dataPtr, uint32 diskPosition, uint32 dataSize);
+	void setLazyDecoding(bool lazyDecoding) { _lazyDecoding = lazyDecoding; }
 	void setSectorLimit(uint sectorLimit) { _sectorLimit = sectorLimit; } // Maximum number of sectors to read per track before stepping
 	uint getBytesPerSector() const { return _bytesPerSector; }
 	uint getSectorsPerTrack() const { return _sectorsPerTrack; }
@@ -116,12 +160,20 @@ protected:
 		const DiskImage *_disk;
 	};
 
-	Common::SeekableReadStream *_stream;
+	Common::String _name;
+	Common::SeekableReadStream *_inputStream;
+	Common::SeekableReadStream *_decodeStream;
+	byte *_decodeBuffer;
+	Common::BitArray _decodedTracks;
+	DiskImageEncoding _encoding;
+	bool _lazyDecoding;
+
 	uint _tracks, _sectorsPerTrack, _bytesPerSector, _firstSector;
 	uint _sectorLimit;
-	
+
 private:
 	bool open(const Common::String &name, Common::File *f);
+	void decodeTrack(uint track);
 };
 
 // Data in plain files


Commit: 2f2f3a59bdcd1401c450dd864ac835aa30646401
    https://github.com/scummvm/scummvm/commit/2f2f3a59bdcd1401c450dd864ac835aa30646401
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2024-08-25T14:57:06+03:00

Commit Message:
AGI: Apple II NIB and WOZ disk image support

Changed paths:
  A engines/agi/disk_image.cpp
    engines/agi/agi.h
    engines/agi/detection.cpp
    engines/agi/disk_image.h
    engines/agi/loader_a2.cpp
    engines/agi/loader_v1.cpp
    engines/agi/module.mk
    engines/agi/objects.cpp


diff --git a/engines/agi/agi.h b/engines/agi/agi.h
index a39d5708ddc..347f4df8a73 100644
--- a/engines/agi/agi.h
+++ b/engines/agi/agi.h
@@ -587,6 +587,7 @@ protected:
 class AgiLoader_A2 : public AgiLoader {
 public:
 	AgiLoader_A2(AgiEngine *vm) : AgiLoader(vm) {}
+	~AgiLoader_A2() override;
 
 	void init() override;
 	int loadDirs() override;
@@ -595,7 +596,7 @@ public:
 	int loadWords() override;
 
 private:
-	Common::Array<Common::String> _imageFiles;
+	Common::Array<Common::SeekableReadStream *> _disks;
 	Common::Array<AgiDiskVolume> _volumes;
 	AgiDir _logDir;
 	AgiDir _picDir;
@@ -610,7 +611,7 @@ private:
 	static bool readVolumeMap(Common::SeekableReadStream &stream, uint32 position, uint32 bufferLength, Common::Array<uint32> &volumeMap);
 
 	A2DirVersion detectDirVersion(Common::SeekableReadStream &stream);
-	bool loadDir(AgiDir *dir, Common::File &disk, uint32 dirOffset, uint32 dirLength, A2DirVersion dirVersion);
+	bool loadDir(AgiDir *dir, Common::SeekableReadStream &disk, uint32 dirOffset, uint32 dirLength, A2DirVersion dirVersion);
 };
 
 class AgiLoader_v1 : public AgiLoader {
@@ -933,7 +934,7 @@ public:
 	// Objects
 public:
 	int loadObjects(const char *fname);
-	int loadObjects(Common::File &fp, int flen);
+	int loadObjects(Common::SeekableReadStream &fp, int flen);
 	const char *objectName(uint16 objectNr);
 	int objectGetLocation(uint16 objectNr);
 	void objectSetLocation(uint16 objectNr, int location);
diff --git a/engines/agi/detection.cpp b/engines/agi/detection.cpp
index bc337ce3806..9d5c0ba9317 100644
--- a/engines/agi/detection.cpp
+++ b/engines/agi/detection.cpp
@@ -348,6 +348,7 @@ void AgiMetaEngineDetection::getPotentialDiskImages(
 			if (f->_key.baseName().hasSuffixIgnoreCase(imageExtensions[i])) {
 				debug(3, "potential disk image: %s", f->_key.baseName().c_str());
 				imageFiles.push_back(f->_key);
+				break;
 			}
 		}
 	}
@@ -368,16 +369,8 @@ ADDetectedGame AgiMetaEngineDetection::detectPcDiskImageGame(const FileMap &allF
 
 	// find disk one by reading potential images until a match is found
 	for (const Common::Path &imageFile : imageFiles) {
-		Common::SeekableReadStream *stream = allFiles[imageFile].createReadStream();
+		Common::SeekableReadStream *stream = openPCDiskImage(imageFile, allFiles[imageFile]);
 		if (stream == nullptr) {
-			warning("unable to open disk image: %s", imageFile.baseName().c_str());
-			continue;
-		}
-
-		// image file size must be 360k
-		int64 fileSize = stream->size();
-		if (fileSize != PC_DISK_SIZE) {
-			delete stream;
 			continue;
 		}
 
@@ -409,7 +402,7 @@ ADDetectedGame AgiMetaEngineDetection::detectPcDiskImageGame(const FileMap &allF
 							FileProperties fileProps;
 							fileProps.md5 = file->md5;
 							fileProps.md5prop = kMD5Archive;
-							fileProps.size = fileSize;
+							fileProps.size = PC_DISK_SIZE;
 							detectedGame.matchedFiles[imageFile] = fileProps;
 							return detectedGame;
 						}
@@ -483,20 +476,16 @@ ADDetectedGame AgiMetaEngineDetection::detectA2DiskImageGame(const FileMap &allF
 
 	// find disk one by reading potential images until a match is found
 	for (const Common::Path &imageFile : imageFiles) {
-		Common::SeekableReadStream *stream = allFiles[imageFile].createReadStream();
+		// lazily-load disk image tracks as they're accessed.
+		// prevents decoding entire disks just to read a few dynamic sectors.
+		// this would create a significant delay for images in the .woz format.
+		const bool loadAllTracks = false;
+		Common::SeekableReadStream *stream = openA2DiskImage(imageFile, allFiles[imageFile], loadAllTracks);
 		if (stream == nullptr) {
 			warning("unable to open disk image: %s", imageFile.baseName().c_str());
 			continue;
 		}
 
-		// image file size must be 140k.
-		// this simple check will be removed when more image formats are supported.
-		int64 fileSize = stream->size();
-		if (fileSize != A2_DISK_SIZE) {
-			delete stream;
-			continue;
-		}
-
 		// attempt to locate and hash logdir by reading initdir,
 		// and also known logdir locations for games without initdir.
 		Common::String logdirHashInitdir = getLogDirHashFromA2DiskImage(*stream);
@@ -532,7 +521,7 @@ ADDetectedGame AgiMetaEngineDetection::detectA2DiskImageGame(const FileMap &allF
 							FileProperties fileProps;
 							fileProps.md5 = file->md5;
 							fileProps.md5prop = kMD5Archive;
-							fileProps.size = fileSize;
+							fileProps.size = A2_DISK_SIZE;
 							detectedGame.matchedFiles[imageFile] = fileProps;
 							return detectedGame;
 						}
diff --git a/engines/agi/disk_image.cpp b/engines/agi/disk_image.cpp
new file mode 100644
index 00000000000..e2aad80ba38
--- /dev/null
+++ b/engines/agi/disk_image.cpp
@@ -0,0 +1,123 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/formats/disk_image.h"
+#include "common/memstream.h"
+#include "common/path.h"
+#include "common/textconsole.h"
+
+#include "agi/disk_image.h"
+
+namespace Agi {
+
+/**
+ * DiskImageStream is a stream wrapper around Common::DiskImage.
+ *
+ * This allows DiskImage to lazily decode tracks as a stream is used.
+ * This is important for detection, because the .woz format is noticeably
+ * expensive to decode all tracks at once, and detection has to read
+ * INITDIR to discover which track to read LOGDIR from.
+ */
+class DiskImageStream : virtual public Common::SeekableReadStream {
+public:
+	DiskImageStream(Common::DiskImage *diskImage) : _diskImage(diskImage), _stream(_diskImage->getDiskStream()) {}
+
+	~DiskImageStream() {
+		delete _diskImage;
+	}
+
+	uint32 read(void *dataPtr, uint32 dataSize) override {
+		return _diskImage->read(dataPtr, pos(), dataSize);
+	}
+
+	bool eos() const { return _stream->eos(); }
+	void clearErr() { _stream->clearErr(); }
+
+	int64 pos() const { return _stream->pos(); }
+	int64 size() const { return _stream->size(); }
+
+	bool seek(int64 offs, int whence = SEEK_SET) { return _stream->seek(offs, whence); }
+
+private:
+	Common::DiskImage *_diskImage;
+	Common::SeekableReadStream *_stream;
+};
+
+Common::SeekableReadStream *openPCDiskImage(const Common::Path &path, const Common::FSNode &node) {
+	Common::SeekableReadStream *stream = node.createReadStream();
+	if (stream == nullptr) {
+		warning("unable to open disk image: %s", path.baseName().c_str());
+		return nullptr;
+	}
+
+	// validate disk size
+	if (stream->size() != PC_DISK_SIZE) {
+		delete stream;
+		return nullptr;
+	}
+
+	return stream;
+}
+
+Common::SeekableReadStream *openA2DiskImage(const Common::Path &path, const Common::FSNode &node, bool loadAllTracks) {
+	Common::String name = path.baseName();
+
+	// Open the image with Common::DiskImage, unless the file extension is ".img".
+	// DiskImage expects ".img" to be a PC disk image, but it also gets used as
+	// an Apple II raw sector disk image, so just open it and and read it.
+	Common::SeekableReadStream *stream = nullptr;
+	if (name.hasSuffixIgnoreCase(".img")) {
+		stream = node.createReadStream();
+	} else {
+		if (loadAllTracks) {
+			// when loading all tracks, open with DiskImage and take the stream.
+			Common::DiskImage diskImage;
+			if (diskImage.open(node)) {
+				stream = diskImage.releaseStream();
+			}
+		} else {
+			// when loading tracks as they're used, create a DiskImage with lazy
+			// decoding and wrap it in a stream.
+			Common::DiskImage *diskImage = new Common::DiskImage();
+			diskImage->setLazyDecoding(true);
+			if (diskImage->open(node)) {
+				stream = new DiskImageStream(diskImage);
+			} else {
+				delete diskImage;
+			}
+		}
+	}
+
+	if (stream == nullptr) {
+		warning("unable to open disk image: %s", path.baseName().c_str());
+		return nullptr;
+	}
+
+	// validate disk size
+	if (stream->size() != A2_DISK_SIZE) {
+		delete stream;
+		return nullptr;
+	}
+
+	return stream;
+}
+
+} // End of namespace Agi
diff --git a/engines/agi/disk_image.h b/engines/agi/disk_image.h
index c8340857433..941c081d5cf 100644
--- a/engines/agi/disk_image.h
+++ b/engines/agi/disk_image.h
@@ -22,6 +22,11 @@
 #ifndef AGI_DISK_IMAGE_H
 #define AGI_DISK_IMAGE_H
 
+namespace Common {
+class SeekableReadStream;
+class Path;
+}
+
 namespace Agi {
 
 // PC disk image values and helpers for AgiLoader_v1 and AgiMetaEngineDetection
@@ -57,7 +62,7 @@ static const char * const pcDiskImageExtensions[] = { ".ima", ".img" };
 // A2 disk image values and helpers for AgiLoader_A2 and AgiMetaEngineDetection
 
 // Disk image detection requires that image files have a known extension
-static const char * const a2DiskImageExtensions[] = { ".do", ".dsk" };
+static const char * const a2DiskImageExtensions[] = { ".do", ".dsk", ".img", ".nib", ".woz" };
 
 #define A2_DISK_SIZE                     (35 * 16 * 256)
 #define A2_DISK_POSITION(t, s, o)        ((((t * 16) + s) * 256) + o)
@@ -93,6 +98,9 @@ static const char * const a2DiskImageExtensions[] = { ".do", ".dsk" };
 #define A2_BC_DISK_COUNT                 5
 #define A2_BC_VOLUME_COUNT               9
 
+Common::SeekableReadStream *openPCDiskImage(const Common::Path &path, const Common::FSNode &node);
+Common::SeekableReadStream *openA2DiskImage(const Common::Path &path, const Common::FSNode &node, bool loadAllTracks = true);
+
 } // End of namespace Agi
 
 #endif /* AGI_DISK_IMAGE_H */
diff --git a/engines/agi/loader_a2.cpp b/engines/agi/loader_a2.cpp
index c1437dd929d..9fb0fe5daeb 100644
--- a/engines/agi/loader_a2.cpp
+++ b/engines/agi/loader_a2.cpp
@@ -24,7 +24,9 @@
 #include "agi/words.h"
 
 #include "common/config-manager.h"
+#include "common/formats/disk_image.h"
 #include "common/fs.h"
+#include "common/memstream.h"
 
 namespace Agi {
 
@@ -33,9 +35,8 @@ namespace Agi {
 // Floppy disks have two sides; each side is a disk with its own image file.
 // All disk sides are 140k with 35 tracks and 16 sectors per track.
 //
-// Currently, the only supported image format is "raw", with sectors in logical
-// order. Each image file must be exactly 143,360 bytes.
-// TODO: Add support for other image formats with ADL's disk iamge code.
+// Multiple disk image formats are supported; see Common::DiskImage. The file
+// extension determines the format. For example: .do, .dsk, .nib, .woz.
 //
 // The disks do not use a standard file system. Instead, file locations are
 // stored in an INITDIR structure at a fixed location. KQ2 and BC don't have
@@ -59,6 +60,12 @@ namespace Agi {
 
 typedef Common::HashMap<Common::Path, Common::FSNode, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> FileMap;
 
+AgiLoader_A2::~AgiLoader_A2() {
+	for (uint d = 0; d < _disks.size(); d++) {
+		delete _disks[d];
+	}
+}
+
 void AgiLoader_A2::init() {
 	// get all files in game directory
 	Common::FSList allFiles;
@@ -77,6 +84,7 @@ void AgiLoader_A2::init() {
 				Common::Path path = file.getPath();
 				imageFiles.push_back(path);
 				fileMap[path] = file;
+				break;
 			}
 		}
 	}
@@ -90,28 +98,21 @@ void AgiLoader_A2::init() {
 	uint diskOneIndex;
 	for (diskOneIndex = 0; diskOneIndex < imageFiles.size(); diskOneIndex++) {
 		const Common::Path &imageFile = imageFiles[diskOneIndex];
-		Common::SeekableReadStream *stream = fileMap[imageFile].createReadStream();
+		Common::SeekableReadStream *stream = openA2DiskImage(imageFile, fileMap[imageFile]);
 		if (stream == nullptr) {
 			warning("AgiLoader_A2: unable to open disk image: %s", imageFile.baseName().c_str());
 			continue;
 		}
 
-		// image file size must be 140k
-		int64 fileSize = stream->size();
-		if (fileSize != A2_DISK_SIZE) {
-			delete stream;
-			continue;
-		}
-
 		// read image as disk one
 		diskCount = readDiskOne(*stream, volumeMap);
-		delete stream;
-
 		if (diskCount > 0) {
 			debugC(3, "AgiLoader_A2: disk one found: %s", imageFile.baseName().c_str());
-			_imageFiles.resize(diskCount);
-			_imageFiles[0] = imageFile.baseName();
+			_disks.resize(diskCount);
+			_disks[0] = stream;
 			break;
+		} else {
+			delete stream;
 		}
 	}
 
@@ -132,23 +133,16 @@ void AgiLoader_A2::init() {
 		uint imageFileIndex = (diskOneIndex + i) % imageFiles.size();
 		Common::Path &imageFile = imageFiles[imageFileIndex];
 
-		Common::SeekableReadStream *stream = fileMap[imageFile].createReadStream();
+		Common::SeekableReadStream *stream = openA2DiskImage(imageFile, fileMap[imageFile]);
 		if (stream == nullptr) {
-			warning("AgiLoader_A2: unable to open disk image: %s", imageFile.baseName().c_str());
-			continue;
-		}
-
-		// image file size must be 140k
-		int32 fileSize = stream->size();
-		if (fileSize != A2_DISK_SIZE) {
-			delete stream;
 			continue;
 		}
 
 		// check each disk
+		bool diskFound = false;
 		for (int d = 1; d < diskCount; d++) {
 			// has disk already been found?
-			if (!_imageFiles[d].empty()) {
+			if (_disks[d] != nullptr) {
 				continue;
 			}
 
@@ -173,13 +167,16 @@ void AgiLoader_A2::init() {
 			}
 
 			if (match) {
-				_imageFiles[d] = imageFile.baseName();
+				_disks[d] = stream;
 				disksFound++;
+				diskFound = true;
 				break;
 			}
 		}
 
-		delete stream;
+		if (!diskFound) {
+			delete stream;
+		}
 	}
 
 	// populate _volumes with the locations of the ones we will use.
@@ -332,21 +329,18 @@ bool AgiLoader_A2::readVolumeMap(
 
 int AgiLoader_A2::loadDirs() {
 	// if init didn't find disks then fail
-	if (_imageFiles.empty()) {
+	if (_disks.empty()) {
 		return errFilesNotFound;
 	}
-	for (uint32 i = 0; i < _imageFiles.size(); i++) {
-		if (_imageFiles.empty()) {
-			warning("AgiLoader_A2: disk %d not found", i);
+	for (uint d = 0; d < _disks.size(); d++) {
+		if (_disks[d] == nullptr) {
+			warning("AgiLoader_A2: disk %d not found", d);
 			return errFilesNotFound;
 		}
 	}
 
-	// open disk one
-	Common::File disk;
-	if (!disk.open(Common::Path(_imageFiles[0]))) {
-		return errBadFileOpen;
-	}
+	// all dirs are on disk one
+	Common::SeekableReadStream &disk = *_disks[0];
 
 	// detect dir format
 	A2DirVersion dirVersion = detectDirVersion(disk);
@@ -388,7 +382,7 @@ A2DirVersion AgiLoader_A2::detectDirVersion(Common::SeekableReadStream &stream)
 	return A2DirVersionOld;
 }
 
-bool AgiLoader_A2::loadDir(AgiDir *dir, Common::File &disk, uint32 dirOffset, uint32 dirLength, A2DirVersion dirVersion) {
+bool AgiLoader_A2::loadDir(AgiDir *dir, Common::SeekableReadStream &disk, uint32 dirOffset, uint32 dirLength, A2DirVersion dirVersion) {
 	// seek to directory on disk
 	disk.seek(dirOffset);
 
@@ -440,12 +434,8 @@ uint8 *AgiLoader_A2::loadVolumeResource(AgiDir *agid) {
 		return nullptr;
 	}
 
-	Common::File disk;
 	int diskIndex = _volumes[agid->volume].disk;
-	if (!disk.open(Common::Path(_imageFiles[diskIndex]))) {
-		warning("AgiLoader_A2: unable to open disk image: %s", _imageFiles[diskIndex].c_str());
-		return nullptr;
-	}
+	Common::SeekableReadStream &disk = *_disks[diskIndex];
 
 	// seek to resource and validate header
 	int offset = _volumes[agid->volume].offset + agid->offset;
@@ -469,22 +459,22 @@ uint8 *AgiLoader_A2::loadVolumeResource(AgiDir *agid) {
 }
 
 int AgiLoader_A2::loadObjects() {
-	Common::File disk;
-	if (!disk.open(Common::Path(_imageFiles[0]))) {
-		return errBadFileOpen;
+	if (_disks.empty()) {
+		return errFilesNotFound;
 	}
 
+	Common::SeekableReadStream &disk = *_disks[0];
 	disk.seek(_objects.offset);
 	return _vm->loadObjects(disk, _objects.len);
 }
 
 int AgiLoader_A2::loadWords() {
-	Common::File disk;
-	if (!disk.open(Common::Path(_imageFiles[0]))) {
-		return errBadFileOpen;
+	if (_disks.empty()) {
+		return errFilesNotFound;
 	}
 
 	// TODO: pass length and validate in parser
+	Common::SeekableReadStream &disk = *_disks[0];
 	disk.seek(_words.offset);
 	if (_vm->getVersion() < 0x2000) {
 		return _vm->_words->loadDictionary_v1(disk);
diff --git a/engines/agi/loader_v1.cpp b/engines/agi/loader_v1.cpp
index 66b563ad1c8..8291ac5ab8b 100644
--- a/engines/agi/loader_v1.cpp
+++ b/engines/agi/loader_v1.cpp
@@ -80,16 +80,8 @@ void AgiLoader_v1::init() {
 	uint diskOneIndex;
 	for (diskOneIndex = 0; diskOneIndex < imageFiles.size(); diskOneIndex++) {
 		const Common::Path &imageFile = imageFiles[diskOneIndex];
-		Common::SeekableReadStream *stream = fileMap[imageFile].createReadStream();
+		Common::SeekableReadStream *stream = openPCDiskImage(imageFile, fileMap[imageFile]);
 		if (stream == nullptr) {
-			warning("AgiLoader_v1: unable to open disk image: %s", imageFile.baseName().c_str());
-			continue;
-		}
-
-		// image file size must be 360k
-		int32 fileSize = stream->size();
-		if (fileSize != PC_DISK_SIZE) {
-			delete stream;
 			continue;
 		}
 
@@ -139,26 +131,17 @@ void AgiLoader_v1::init() {
 		uint diskTwoIndex = (diskOneIndex + i) % imageFiles.size();
 		Common::Path &imageFile = imageFiles[diskTwoIndex];
 
-		Common::SeekableReadStream *stream = fileMap[imageFile].createReadStream();
+		Common::SeekableReadStream *stream = openPCDiskImage(imageFile, fileMap[imageFile]);
 		if (stream == nullptr) {
-			warning("AgiLoader_v1: unable to open disk image: %s", imageFile.baseName().c_str());
-			continue;
-		}
-
-		// image file size must be 360k
-		int64 fileSize = stream->size();
-		if (fileSize != PC_DISK_SIZE) {
-			delete stream;
 			continue;
 		}
 
 		// read resource header
 		uint16 magic = stream->readUint16BE();
 		byte volume = stream->readByte();
-		uint16 size = stream->readUint16LE();
 		delete stream;
 
-		if (magic == 0x1234 && volume == 2 && 5 + size <= PC_DISK_SIZE) {
+		if (magic == 0x1234 && volume == 2) {
 			debugC(3, "AgiLoader_v1: disk two found: %s", imageFile.baseName().c_str());
 			_imageFiles.push_back(imageFile.baseName());
 			_volumes.push_back(AgiDiskVolume(_imageFiles.size() - 1, 0));
diff --git a/engines/agi/module.mk b/engines/agi/module.mk
index 8711a4a3f51..cf8269f2ed4 100644
--- a/engines/agi/module.mk
+++ b/engines/agi/module.mk
@@ -5,6 +5,7 @@ MODULE_OBJS := \
 	checks.o \
 	console.o \
 	cycle.o \
+	disk_image.o \
 	font.o \
 	global.o \
 	graphics.o \
@@ -57,3 +58,10 @@ DETECT_OBJS += $(MODULE)/detection.o
 # This is unneeded by the engine module itself,
 # so separate it completely.
 DETECT_OBJS += $(MODULE)/wagparser.o
+
+# Skip building the following objects if a static
+# module is enabled, because it already has the contents.
+ifneq ($(ENABLE_AGI), STATIC_PLUGIN)
+# External dependencies for detection.
+DETECT_OBJS += $(MODULE)/disk_image.o
+endif
diff --git a/engines/agi/objects.cpp b/engines/agi/objects.cpp
index 5b1adb16d25..3ceea0627e0 100644
--- a/engines/agi/objects.cpp
+++ b/engines/agi/objects.cpp
@@ -95,16 +95,14 @@ int AgiEngine::loadObjects(const char *fname) {
  * @param  fp    File pointer
  * @param  flen  File length
  */
-int AgiEngine::loadObjects(Common::File &fp, int flen) {
+int AgiEngine::loadObjects(Common::SeekableReadStream &fp, int flen) {
 	uint8 *mem;
 
 	if ((mem = (uint8 *)calloc(1, flen + 32)) == nullptr) {
-		fp.close();
 		return errNotEnoughMemory;
 	}
 
 	fp.read(mem, flen);
-	fp.close();
 
 	decodeObjects(mem, flen);
 	free(mem);


Commit: 16d564d826211d64390c146e4ca4c62e5ace4dc6
    https://github.com/scummvm/scummvm/commit/16d564d826211d64390c146e4ca4c62e5ace4dc6
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2024-08-25T14:57:06+03:00

Commit Message:
AGI: Map engine error codes to Common codes

Changed paths:
    engines/agi/agi.cpp


diff --git a/engines/agi/agi.cpp b/engines/agi/agi.cpp
index d36e23b149c..5b3c0f5c05a 100644
--- a/engines/agi/agi.cpp
+++ b/engines/agi/agi.cpp
@@ -596,7 +596,12 @@ Common::Error AgiEngine::go() {
 
 	int ec = runGame();
 
-	return (ec == errOK) ? Common::kNoError : Common::kUnknownError;
+	switch (ec) {
+	case errOK:            return Common::kNoError;
+	case errFilesNotFound: return Common::kNoGameDataFoundError;
+	case errBadFileOpen:   return Common::kReadingFailed;
+	default:               return Common::kUnknownError;
+	}
 }
 
 void AgiEngine::syncSoundSettings() {




More information about the Scummvm-git-logs mailing list