diff --git a/Analyser/Static/StaticAnalyser.cpp b/Analyser/Static/StaticAnalyser.cpp
index 5a0a7996d..589f3f206 100644
--- a/Analyser/Static/StaticAnalyser.cpp
+++ b/Analyser/Static/StaticAnalyser.cpp
@@ -55,6 +55,7 @@
 
 // Mass Storage Devices (i.e. usually, hard disks)
 #include "../../Storage/MassStorage/Formats/DAT.hpp"
+#include "../../Storage/MassStorage/Formats/DSK.hpp"
 #include "../../Storage/MassStorage/Formats/HFV.hpp"
 
 // State Snapshots
@@ -144,7 +145,8 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
 			TargetPlatform::AmstradCPC | TargetPlatform::Oric | TargetPlatform::ZXSpectrum)						// DSK (Amstrad CPC, etc)
 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII)			// DSK (Apple II)
 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)	// DSK (Macintosh, floppy disk)
-	Format("dsk", result.mass_storage_devices, MassStorage::HFV, TargetPlatform::Macintosh)						// DSK (Macintosh, hard disk)
+	Format("dsk", result.mass_storage_devices, MassStorage::HFV, TargetPlatform::Macintosh)						// DSK (Macintosh, hard disk, single volume image)
+	Format("dsk", result.mass_storage_devices, MassStorage::DSK, TargetPlatform::Macintosh)						// DSK (Macintosh, hard disk, full device image)
 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MSXDSK>, TargetPlatform::MSX)				// DSK (MSX)
 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::OricMFMDSK>, TargetPlatform::Oric)			// DSK (Oric)
 	Format("g64", result.disks, Disk::DiskImageHolder<Storage::Disk::G64>, TargetPlatform::Commodore)			// G64
diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj
index e32353f5f..bde5d0985 100644
--- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
+++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
@@ -551,6 +551,8 @@
 		4B92E26B234AE35100CD6D1B /* MFP68901.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B92E268234AE35000CD6D1B /* MFP68901.cpp */; };
 		4B92EACA1B7C112B00246143 /* 6502TimingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92EAC91B7C112B00246143 /* 6502TimingTests.swift */; };
 		4B9378E422A199C600973513 /* Audio.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B9378E222A199C600973513 /* Audio.cpp */; };
+		4B96F7CE263E33B10092AEE1 /* DSK.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B96F7CC263E33B10092AEE1 /* DSK.cpp */; };
+		4B96F7CF263E33B10092AEE1 /* DSK.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B96F7CC263E33B10092AEE1 /* DSK.cpp */; };
 		4B98A05E1FFAD3F600ADF63B /* CSROMFetcher.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B98A05D1FFAD3F600ADF63B /* CSROMFetcher.mm */; };
 		4B98A05F1FFAD62400ADF63B /* CSROMFetcher.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B98A05D1FFAD3F600ADF63B /* CSROMFetcher.mm */; };
 		4B98A0611FFADCDE00ADF63B /* MSXStaticAnalyserTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B98A0601FFADCDE00ADF63B /* MSXStaticAnalyserTests.mm */; };
@@ -1525,6 +1527,8 @@
 		4B95FA9C1F11893B0008E395 /* ZX8081OptionsPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZX8081OptionsPanel.swift; sourceTree = "<group>"; };
 		4B961408222760E0001A7BF2 /* Screenshot.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Screenshot.hpp; sourceTree = "<group>"; };
 		4B96F7CB263E30B00092AEE1 /* RawSectorDump.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RawSectorDump.hpp; sourceTree = "<group>"; };
+		4B96F7CC263E33B10092AEE1 /* DSK.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = DSK.cpp; sourceTree = "<group>"; };
+		4B96F7CD263E33B10092AEE1 /* DSK.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = DSK.hpp; sourceTree = "<group>"; };
 		4B98A05C1FFAD3F600ADF63B /* CSROMFetcher.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CSROMFetcher.hpp; sourceTree = "<group>"; };
 		4B98A05D1FFAD3F600ADF63B /* CSROMFetcher.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = CSROMFetcher.mm; sourceTree = "<group>"; };
 		4B98A0601FFADCDE00ADF63B /* MSXStaticAnalyserTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MSXStaticAnalyserTests.mm; sourceTree = "<group>"; };
@@ -2970,10 +2974,12 @@
 		4B74CF7E2312FA9C00500CE8 /* Formats */ = {
 			isa = PBXGroup;
 			children = (
-				4B74CF7F2312FA9C00500CE8 /* HFV.hpp */,
-				4B74CF802312FA9C00500CE8 /* HFV.cpp */,
 				4BE8EB6425C750B50040BC40 /* DAT.cpp */,
 				4BE8EB6525C750B50040BC40 /* DAT.hpp */,
+				4B96F7CC263E33B10092AEE1 /* DSK.cpp */,
+				4B96F7CD263E33B10092AEE1 /* DSK.hpp */,
+				4B74CF802312FA9C00500CE8 /* HFV.cpp */,
+				4B74CF7F2312FA9C00500CE8 /* HFV.hpp */,
 				4B96F7CB263E30B00092AEE1 /* RawSectorDump.hpp */,
 			);
 			path = Formats;
@@ -5240,6 +5246,7 @@
 				4B1B58F7246CC4E8009C171E /* State.cpp in Sources */,
 				4B0ACC03237756F6008902D0 /* Line.cpp in Sources */,
 				4B055AB11FAE86070060FFFF /* Tape.cpp in Sources */,
+				4B96F7CF263E33B10092AEE1 /* DSK.cpp in Sources */,
 				4B2B946626377C0200E7097C /* SZX.cpp in Sources */,
 				4BEDA43225B3C700000C2DBD /* Executor.cpp in Sources */,
 				4BC1317B2346DF2B00E4FF3D /* MSA.cpp in Sources */,
@@ -5546,6 +5553,7 @@
 				4B89453E201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
 				4BF8D4D5251C11DD00BBE21B /* 65816Storage.cpp in Sources */,
 				4B0ACC2823775819008902D0 /* DMAController.cpp in Sources */,
+				4B96F7CE263E33B10092AEE1 /* DSK.cpp in Sources */,
 				4BC131702346DE5000E4FF3D /* StaticAnalyser.cpp in Sources */,
 				4B37EE821D7345A6006A09A4 /* BinaryDump.cpp in Sources */,
 				4BCE0053227CE8CA000CA200 /* AppleII.cpp in Sources */,
diff --git a/Storage/MassStorage/Formats/DSK.cpp b/Storage/MassStorage/Formats/DSK.cpp
new file mode 100644
index 000000000..231855546
--- /dev/null
+++ b/Storage/MassStorage/Formats/DSK.cpp
@@ -0,0 +1,23 @@
+//
+//  DSK.cpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 01/05/2021.
+//  Copyright © 2021 Thomas Harte. All rights reserved.
+//
+
+#include "DSK.hpp"
+
+using namespace Storage::MassStorage;
+
+DSK::DSK(const std::string &file_name) : RawSectorDump(file_name) {
+	// Minimum validation: check the first sector for a device signature,
+	// with 512-byte blocks.
+	const auto sector = get_block(0);
+	if(sector.size() != 512) {
+		throw std::exception();
+	}
+	if(sector[0] != 0x45 || sector[1] != 0x52 || sector[2] != 0x02 || sector[3] != 0x00) {
+		throw std::exception();
+	}
+}
diff --git a/Storage/MassStorage/Formats/DSK.hpp b/Storage/MassStorage/Formats/DSK.hpp
new file mode 100644
index 000000000..bdf1b53cc
--- /dev/null
+++ b/Storage/MassStorage/Formats/DSK.hpp
@@ -0,0 +1,30 @@
+//
+//  DSK.hpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 01/05/2021.
+//  Copyright © 2021 Thomas Harte. All rights reserved.
+//
+
+#ifndef MassStorage_DSK_hpp
+#define MassStorage_DSK_hpp
+
+#include "RawSectorDump.hpp"
+
+namespace Storage {
+namespace MassStorage {
+
+/*!
+	Provides a @c MassStorageDevice containing a Macintosh DSK image, which is just a
+	sector dump of an entire HFS drive. It will be validated for an Apple-style partition map and communicate
+	in 512-byte blocks.
+*/
+class DSK: public RawSectorDump<512> {
+	public:
+		DSK(const std::string &file_name);
+};
+
+}
+}
+
+#endif /* MassStorage_DSK_hpp */
diff --git a/Storage/MassStorage/Formats/HFV.cpp b/Storage/MassStorage/Formats/HFV.cpp
index 3e454e892..f4e3b9ae0 100644
--- a/Storage/MassStorage/Formats/HFV.cpp
+++ b/Storage/MassStorage/Formats/HFV.cpp
@@ -15,7 +15,10 @@ HFV::HFV(const std::string &file_name) : file_(file_name) {
 	const auto file_size = file_.stats().st_size;
 	if(file_size & 511 || file_size <= 800*1024) throw std::exception();
 
-	// TODO: check filing system for MFS, HFS or HFS+.
+	// Is this an HFS volume?
+	// TODO: check filing system for MFS or HFS+.
+	const auto prefix = file_.read(2);
+	if(prefix[0] != 'L' || prefix[1] != 'K')  throw std::exception();
 }
 
 size_t HFV::get_block_size() {
diff --git a/Storage/MassStorage/SCSI/DirectAccessDevice.cpp b/Storage/MassStorage/SCSI/DirectAccessDevice.cpp
index f6944d2f0..a6d99ccc2 100644
--- a/Storage/MassStorage/SCSI/DirectAccessDevice.cpp
+++ b/Storage/MassStorage/SCSI/DirectAccessDevice.cpp
@@ -19,7 +19,7 @@ bool DirectAccessDevice::read(const Target::CommandState &state, Target::Respond
 	if(!device_) return false;
 
 	const auto specs = state.read_write_specs();
-	LOG("Read: " << specs.number_of_blocks << " from " << specs.address);
+	LOG("Read: " << std::dec << specs.number_of_blocks << " from " << specs.address);
 
 	std::vector<uint8_t> output = device_->get_block(specs.address);
 	for(uint32_t offset = 1; offset < specs.number_of_blocks; ++offset) {