diff --git a/Analyser/Static/StaticAnalyser.cpp b/Analyser/Static/StaticAnalyser.cpp index 0e1163d15..e5251caeb 100644 --- a/Analyser/Static/StaticAnalyser.cpp +++ b/Analyser/Static/StaticAnalyser.cpp @@ -50,6 +50,7 @@ #include "../../Storage/Disk/DiskImage/Formats/FAT12.hpp" #include "../../Storage/Disk/DiskImage/Formats/HFE.hpp" #include "../../Storage/Disk/DiskImage/Formats/IPF.hpp" +#include "../../Storage/Disk/DiskImage/Formats/IMD.hpp" #include "../../Storage/Disk/DiskImage/Formats/MacintoshIMG.hpp" #include "../../Storage/Disk/DiskImage/Formats/MSA.hpp" #include "../../Storage/Disk/DiskImage/Formats/NIB.hpp" @@ -182,6 +183,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: // HFE (TODO: switch to AllDisk once the MSX stops being so greedy) Format("ima", result.disks, Disk::DiskImageHolder, TargetPlatform::PCCompatible) // IMG (MS-DOS style) Format("image", result.disks, Disk::DiskImageHolder, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2) + Format("imd", result.disks, Disk::DiskImageHolder, TargetPlatform::PCCompatible) // IMD Format("img", result.disks, Disk::DiskImageHolder, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2) // Treat PC booter as a potential backup only if this doesn't parse as a FAT12. diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 812890027..6a99225bb 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -37,6 +37,8 @@ 42A5E8532ABBE16F00A0DD5D /* lax_test.bin in Resources */ = {isa = PBXBuildFile; fileRef = 42A5E8422ABBE16F00A0DD5D /* lax_test.bin */; }; 42A5E8542ABBE16F00A0DD5D /* branch_backwards_test.bin in Resources */ = {isa = PBXBuildFile; fileRef = 42A5E8432ABBE16F00A0DD5D /* branch_backwards_test.bin */; }; 42E5C3932AC46A7700DA093D /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 42E5C3922AC46A7700DA093D /* Carbon.framework */; }; + 42EB81282B23AAC300429AF4 /* IMD.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 42EB81262B23AAC300429AF4 /* IMD.cpp */; }; + 42EB81292B23AAC300429AF4 /* IMD.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 42EB81262B23AAC300429AF4 /* IMD.cpp */; }; 4B018B89211930DE002A3937 /* 65C02_extended_opcodes_test.bin in Resources */ = {isa = PBXBuildFile; fileRef = 4B018B88211930DE002A3937 /* 65C02_extended_opcodes_test.bin */; }; 4B01A6881F22F0DB001FD6E3 /* Z80MemptrTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B01A6871F22F0DB001FD6E3 /* Z80MemptrTests.swift */; }; 4B0333AF2094081A0050B93D /* AppleDSK.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0333AD2094081A0050B93D /* AppleDSK.cpp */; }; @@ -1195,6 +1197,8 @@ 42AD55312A0C4D5000ACE410 /* 68000Implementation.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = 68000Implementation.hpp; sourceTree = ""; }; 42E5C3922AC46A7700DA093D /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; 42EB81252B21788200429AF4 /* RTC.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RTC.hpp; sourceTree = ""; }; + 42EB81262B23AAC300429AF4 /* IMD.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = IMD.cpp; sourceTree = ""; }; + 42EB81272B23AAC300429AF4 /* IMD.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = IMD.hpp; sourceTree = ""; }; 4B018B88211930DE002A3937 /* 65C02_extended_opcodes_test.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = 65C02_extended_opcodes_test.bin; path = "Klaus Dormann/65C02_extended_opcodes_test.bin"; sourceTree = ""; }; 4B01A6871F22F0DB001FD6E3 /* Z80MemptrTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Z80MemptrTests.swift; sourceTree = ""; }; 4B0333AD2094081A0050B93D /* AppleDSK.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AppleDSK.cpp; sourceTree = ""; }; @@ -3064,6 +3068,7 @@ 4BEBFB4B2002C4BF000708CC /* FAT12.cpp */, 4B4518931F75FD1B00926311 /* G64.cpp */, 4B4518951F75FD1B00926311 /* HFE.cpp */, + 42EB81262B23AAC300429AF4 /* IMD.cpp */, 4B5B372F2777C7FC0047F238 /* IPF.cpp */, 4BB4BFAE22A42F290069048D /* MacintoshIMG.cpp */, 4B58601C1F806AB200AEE2E3 /* MFMSectorDump.cpp */, @@ -3084,6 +3089,7 @@ 4BEBFB4C2002C4BF000708CC /* FAT12.hpp */, 4B4518941F75FD1B00926311 /* G64.hpp */, 4B4518961F75FD1B00926311 /* HFE.hpp */, + 42EB81272B23AAC300429AF4 /* IMD.hpp */, 4B5B37302777C7FC0047F238 /* IPF.hpp */, 4BB4BFAF22A42F290069048D /* MacintoshIMG.hpp */, 4B58601D1F806AB200AEE2E3 /* MFMSectorDump.hpp */, @@ -5959,6 +5965,7 @@ 4B051C912669C90B00CA44E8 /* ROMCatalogue.cpp in Sources */, 4B055AB91FAE86170060FFFF /* Acorn.cpp in Sources */, 4B302185208A550100773308 /* DiskII.cpp in Sources */, + 42EB81292B23AAC300429AF4 /* IMD.cpp in Sources */, 4B051CB1267C1CA200CA44E8 /* Keyboard.cpp in Sources */, 4B0F1BB32602645900B85C66 /* StaticAnalyser.cpp in Sources */, 4B055A931FAE85B50060FFFF /* BinaryDump.cpp in Sources */, @@ -6057,6 +6064,7 @@ 4B0333AF2094081A0050B93D /* AppleDSK.cpp in Sources */, 4B894518201967B4007DE474 /* ConfidenceCounter.cpp in Sources */, 4BCE005A227CFFCA000CA200 /* Macintosh.cpp in Sources */, + 42EB81282B23AAC300429AF4 /* IMD.cpp in Sources */, 4B6AAEA4230E3E1D0078E864 /* MassStorageDevice.cpp in Sources */, 4B89452E201967B4007DE474 /* StaticAnalyser.cpp in Sources */, 4BC890D3230F86020025A55A /* DirectAccessDevice.cpp in Sources */, diff --git a/OSBindings/Mac/Clock Signal/Info.plist b/OSBindings/Mac/Clock Signal/Info.plist index 37dbdb05d..9e367a765 100644 --- a/OSBindings/Mac/Clock Signal/Info.plist +++ b/OSBindings/Mac/Clock Signal/Info.plist @@ -691,6 +691,26 @@ NSDocumentClass $(PRODUCT_MODULE_NAME).MachineDocument + + CFBundleTypeExtensions + + imd + + CFBundleTypeName + IMD disk image + CFBundleTypeOSTypes + + ???? + + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSTypeIsPackage + + NSDocumentClass + $(PRODUCT_MODULE_NAME).MachineDocument + CFBundleTypeExtensions diff --git a/Storage/Disk/DiskImage/Formats/CPCDSK.hpp b/Storage/Disk/DiskImage/Formats/CPCDSK.hpp index 566cb2300..ab4d333cb 100644 --- a/Storage/Disk/DiskImage/Formats/CPCDSK.hpp +++ b/Storage/Disk/DiskImage/Formats/CPCDSK.hpp @@ -24,19 +24,18 @@ namespace Storage::Disk { class CPCDSK: public DiskImage { public: /*! - Construct an @c AcornADF containing content from the file with name @c file_name. + Construct a @c CPCDSK containing content from the file with name @c file_name. @throws Storage::FileHolder::Error::CantOpen if this file can't be opened. @throws Error::InvalidFormat if the file doesn't appear to contain an Acorn .ADF format image. */ CPCDSK(const std::string &file_name); - // implemented to satisfy @c Disk + // DiskImage interface. HeadPosition get_maximum_head_position() final; int get_head_count() final; bool get_is_read_only() final; - - void set_tracks(const std::map> &tracks) final; + void set_tracks(const std::map<::Storage::Disk::Track::Address, std::shared_ptr<::Storage::Disk::Track>> &tracks) final; std::shared_ptr<::Storage::Disk::Track> get_track_at_position(::Storage::Disk::Track::Address address) final; private: diff --git a/Storage/Disk/DiskImage/Formats/IMD.cpp b/Storage/Disk/DiskImage/Formats/IMD.cpp new file mode 100644 index 000000000..34c309bb6 --- /dev/null +++ b/Storage/Disk/DiskImage/Formats/IMD.cpp @@ -0,0 +1,161 @@ +// +// IMD.cpp +// Clock Signal +// +// Created by Thomas Harte on 08/12/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#include "IMD.hpp" + +#include "../../Encodings/MFM/Constants.hpp" +#include "../../Encodings/MFM/Encoder.hpp" +#include "../../Encodings/MFM/SegmentParser.hpp" +#include "../../Track/TrackSerialiser.hpp" + +#include + +using namespace Storage::Disk; + +IMD::IMD(const std::string &file_name) : file_(file_name) { + // Check for signature. + if(!file_.check_signature("IMD")) { + throw Error::InvalidFormat; + } + + // Skip rest of ASCII. + while(file_.get8() != 0x1a); + + // Build track map. + while(true) { + const auto location = file_.tell(); + + // Skip mode. + file_.seek(1, SEEK_CUR); + + // Grab relevant fields. + const uint8_t cylinder = file_.get8(); + const uint8_t head = file_.get8(); + const uint8_t sector_count = file_.get8(); + const uint8_t sector_size = file_.get8(); + if(file_.eof()) { + break; + } + + cylinders_ = std::max(cylinder, cylinders_); + heads_ = std::max(uint8_t(head & 1), heads_); + + // Update head and cylinder extents, record sector location for later. + track_locations_.emplace( + Storage::Disk::Track::Address(head & 1, HeadPosition(cylinder)), + location); + + // Skip sector numbers. + file_.seek(sector_count, SEEK_CUR); + + // Skip cylinder map. + if(head & 0x80) { + file_.seek(sector_count, SEEK_CUR); + } + + // Skip head map. + if(head & 0x40) { + file_.seek(sector_count, SEEK_CUR); + } + + // Skip sectors. + for(int c = 0; c < sector_count; c++) { + const uint8_t type = file_.get8(); + switch(type) { + case 0x00: break; // Sector couldn't be read. + + // Types with all sector data present. + case 0x01: case 0x03: case 0x05: case 0x07: + file_.seek(128 << sector_size, SEEK_CUR); + break; + + // Types with a single byte present. + case 0x02: case 0x04: case 0x06: case 0x08: + file_.seek(1, SEEK_CUR); + break; + } + } + } + + // Both heads_ and cylinders_ are now the maximum observed IDs, which + // are one less than the counts. + ++ cylinders_; + ++ heads_; +} + +HeadPosition IMD::get_maximum_head_position() { + return HeadPosition(cylinders_); +} + +int IMD::get_head_count() { + return heads_ + 1; +} + +std::shared_ptr<::Storage::Disk::Track> IMD::get_track_at_position(::Storage::Disk::Track::Address address) { + auto location = track_locations_.find(address); + if(location == track_locations_.end()) { + return nullptr; + } + + // Seek to track, parse fully this time. + file_.seek(location->second, SEEK_SET); + + const uint8_t mode = file_.get8(); + const uint8_t cylinder = file_.get8(); + const uint8_t head = file_.get8(); + const uint8_t sector_count = file_.get8(); + const uint8_t sector_size = file_.get8(); + + const std::vector sector_ids = file_.read(sector_count); + const std::vector cylinders = (head & 0x80) ? file_.read(sector_count) : std::vector{}; + const std::vector heads = (head & 0x40) ? file_.read(sector_count) : std::vector{}; + + std::vector sectors; + sectors.reserve(sector_count); + + for(size_t c = 0; c < sector_count; c++) { + sectors.emplace_back(); + Storage::Encodings::MFM::Sector §or = sectors.back(); + + // Set up sector address. + sector.address.track = cylinders.empty() ? cylinder : cylinders[c]; + sector.address.side = heads.empty() ? head & 1 : heads[c]; + sector.address.sector = sector_ids[c]; + sector.size = sector_size; + + const auto byte_size = size_t(128 << sector_size); + uint8_t type = file_.get8(); + + // Type 0: sector was present, but couldn't be read. + // Since body CRC errors are a separate item, just don't include a body at all. + if(!type) { + continue; + } + + // Decrement type to turn it into a bit field: + // + // b0 => is compressed within disk image; + // b1 => had deleted address mark; + // b2 => had data CRC error. + --type; + sector.is_deleted = type & 2; + sector.has_data_crc_error = type & 4; + if(type & 1) { + sector.samples.emplace_back(byte_size, file_.get8()); + } else { + sector.samples.push_back(file_.read(byte_size)); + } + } + + // Mode also indicates data density, but I don't have a good strategy for reconciling that if + // it were to disagree with the density implied by the quantity of sectors. So a broad 'is it MFM' test is + // applied only. + return (mode >= 3) ? + Storage::Encodings::MFM::GetMFMTrackWithSectors(sectors) : + Storage::Encodings::MFM::GetFMTrackWithSectors(sectors); +} diff --git a/Storage/Disk/DiskImage/Formats/IMD.hpp b/Storage/Disk/DiskImage/Formats/IMD.hpp new file mode 100644 index 000000000..2ce703e7a --- /dev/null +++ b/Storage/Disk/DiskImage/Formats/IMD.hpp @@ -0,0 +1,45 @@ +// +// IMD.hpp +// Clock Signal +// +// Created by Thomas Harte on 08/12/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#ifndef IMD_hpp +#define IMD_hpp + +#include "../DiskImage.hpp" +#include "../../../FileHolder.hpp" + +namespace Storage::Disk { + +/*! + Provides an @c DiskImage containing an IMD image, which is a collection of arbitrarily-numbered FM or MFM + sectors collected by track. +*/ + +class IMD: public DiskImage { + public: + /*! + Construct an @c IMD containing content from the file with name @c file_name. + + @throws Storage::FileHolder::Error::CantOpen if this file can't be opened. + @throws Error::InvalidFormat if the file doesn't appear to contain an Acorn .ADF format image. + */ + IMD(const std::string &file_name); + + // DiskImage interface. + HeadPosition get_maximum_head_position() final; + int get_head_count() final; + std::shared_ptr<::Storage::Disk::Track> get_track_at_position(::Storage::Disk::Track::Address address) final; + + private: + FileHolder file_; + std::map track_locations_; + uint8_t cylinders_ = 0, heads_ = 0; +}; + +} + +#endif /* IMD_hpp */