From 4e985ef8eb06f12a4c7083bde0f75996d184b4ca Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 20 Apr 2026 09:19:52 -0400 Subject: [PATCH 1/4] Add empty shell for SAP parsing. --- Analyser/Static/StaticAnalyser.cpp | 2 + .../Clock Signal.xcodeproj/project.pbxproj | 10 +++++ OSBindings/Mac/Clock Signal/Info.plist | 1 + Storage/Disk/DiskImage/Formats/SAP.cpp | 40 +++++++++++++++++++ Storage/Disk/DiskImage/Formats/SAP.hpp | 33 +++++++++++++++ cmake/CLK_SOURCES.cmake | 1 + 6 files changed, 87 insertions(+) create mode 100644 Storage/Disk/DiskImage/Formats/SAP.cpp create mode 100644 Storage/Disk/DiskImage/Formats/SAP.hpp diff --git a/Analyser/Static/StaticAnalyser.cpp b/Analyser/Static/StaticAnalyser.cpp index fdda45eaf..0990ebfaa 100644 --- a/Analyser/Static/StaticAnalyser.cpp +++ b/Analyser/Static/StaticAnalyser.cpp @@ -62,6 +62,7 @@ #include "Storage/Disk/DiskImage/Formats/NIB.hpp" #include "Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp" #include "Storage/Disk/DiskImage/Formats/PCBooter.hpp" +#include "Storage/Disk/DiskImage/Formats/SAP.hpp" #include "Storage/Disk/DiskImage/Formats/SSD.hpp" #include "Storage/Disk/DiskImage/Formats/STX.hpp" #include "Storage/Disk/DiskImage/Formats/WOZ.hpp" @@ -311,6 +312,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: TargetPlatform::AcornElectron | TargetPlatform::Coleco | TargetPlatform::MSX, "rom"); + accumulator.try_standard>(TargetPlatform::ThomsonMO, "sap"); accumulator.try_standard(TargetPlatform::Sega, "sg"); accumulator.try_standard(TargetPlatform::Sega, "sms"); accumulator.try_standard>(TargetPlatform::Acorn, "ssd"); diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 46f3fc391..7e5ebcdd3 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -58,6 +58,9 @@ 4B03E8422F9528D4008AF203 /* LEP.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B03E8412F9528D4008AF203 /* LEP.cpp */; }; 4B03E8432F9528D4008AF203 /* LEP.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B03E8412F9528D4008AF203 /* LEP.cpp */; }; 4B03E8442F9528D4008AF203 /* LEP.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B03E8412F9528D4008AF203 /* LEP.cpp */; }; + 4B03E8472F965E17008AF203 /* SAP.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B03E8462F965E17008AF203 /* SAP.cpp */; }; + 4B03E8482F965E17008AF203 /* SAP.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B03E8462F965E17008AF203 /* SAP.cpp */; }; + 4B03E8492F965E17008AF203 /* SAP.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B03E8462F965E17008AF203 /* SAP.cpp */; }; 4B049CDD1DA3C82F00322067 /* BCDTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B049CDC1DA3C82F00322067 /* BCDTest.swift */; }; 4B04C899285E3DC800AA8FD6 /* 65816ComparativeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B04C898285E3DC800AA8FD6 /* 65816ComparativeTests.mm */; }; 4B051C912669C90B00CA44E8 /* ROMCatalogue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B051C5826670A9300CA44E8 /* ROMCatalogue.cpp */; }; @@ -1396,6 +1399,8 @@ 4B03E8352F8D914C008AF203 /* Video.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Video.cpp; sourceTree = ""; }; 4B03E8402F9528D4008AF203 /* LEP.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = LEP.hpp; sourceTree = ""; }; 4B03E8412F9528D4008AF203 /* LEP.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = LEP.cpp; sourceTree = ""; }; + 4B03E8452F965E17008AF203 /* SAP.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SAP.hpp; sourceTree = ""; }; + 4B03E8462F965E17008AF203 /* SAP.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = SAP.cpp; sourceTree = ""; }; 4B046DC31CFE651500E9E45E /* ScanProducer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ScanProducer.hpp; sourceTree = ""; }; 4B047075201ABC180047AB0D /* Cartridge.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Cartridge.hpp; sourceTree = ""; }; 4B049CDC1DA3C82F00322067 /* BCDTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BCDTest.swift; sourceTree = ""; }; @@ -3457,6 +3462,7 @@ 4B0F94FC208C1A1600FE41D9 /* NIB.cpp */, 4B4518971F75FD1B00926311 /* OricMFMDSK.cpp */, 423820422B1A90BE00964EFE /* PCBooter.cpp */, + 4B03E8462F965E17008AF203 /* SAP.cpp */, 4B4518991F75FD1B00926311 /* SSD.cpp */, 4B7BA03323C58B1E00B98D9E /* STX.cpp */, 4B6ED2EE208E2F8A0047B343 /* WOZ.cpp */, @@ -3481,6 +3487,7 @@ 4B0F94FD208C1A1600FE41D9 /* NIB.hpp */, 4B4518981F75FD1B00926311 /* OricMFMDSK.hpp */, 423820432B1A90BE00964EFE /* PCBooter.hpp */, + 4B03E8452F965E17008AF203 /* SAP.hpp */, 4B45189A1F75FD1B00926311 /* SSD.hpp */, 4B7BA03223C58B1E00B98D9E /* STX.hpp */, 4B6ED2EF208E2F8A0047B343 /* WOZ.hpp */, @@ -6675,6 +6682,7 @@ 4BD235CC2F32A4C80094AFAE /* KernelShaders.cpp in Sources */, 4B055AC21FAE9AE30060FFFF /* KeyboardMachine.cpp in Sources */, 4B89453B201967B4007DE474 /* StaticAnalyser.cpp in Sources */, + 4B03E8482F965E17008AF203 /* SAP.cpp in Sources */, 4B055AEB1FAE9BA20060FFFF /* PartialMachineCycle.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6776,6 +6784,7 @@ 4B6AAEA4230E3E1D0078E864 /* MassStorageDevice.cpp in Sources */, 4B89452E201967B4007DE474 /* StaticAnalyser.cpp in Sources */, 4BC890D3230F86020025A55A /* DirectAccessDevice.cpp in Sources */, + 4B03E8492F965E17008AF203 /* SAP.cpp in Sources */, 4B7BA03723CEB86000B98D9E /* BD500.cpp in Sources */, 4B38F3481F2EC11D00D9235D /* AmstradCPC.cpp in Sources */, 4B8FE2221DA19FB20090D3CE /* MachineController.swift in Sources */, @@ -7050,6 +7059,7 @@ 423BDC4A2AB24699008E37B6 /* 8088Tests.mm in Sources */, 4B778F2723A5EEF60000D260 /* BinaryDump.cpp in Sources */, 4BFCA1241ECBDCB400AC40C1 /* AllRAMProcessor.cpp in Sources */, + 4B03E8472F965E17008AF203 /* SAP.cpp in Sources */, 4B06AB042C6460D60034D014 /* 6560.cpp in Sources */, 4B06AB002C6460B70034D014 /* Keyboard.cpp in Sources */, 4B778F5223A5F22F0000D260 /* StaticAnalyser.cpp in Sources */, diff --git a/OSBindings/Mac/Clock Signal/Info.plist b/OSBindings/Mac/Clock Signal/Info.plist index 606b53e0c..23b8a7e98 100644 --- a/OSBindings/Mac/Clock Signal/Info.plist +++ b/OSBindings/Mac/Clock Signal/Info.plist @@ -880,6 +880,7 @@ CFBundleTypeExtensions fd + sap CFBundleTypeIconFile floppy525 diff --git a/Storage/Disk/DiskImage/Formats/SAP.cpp b/Storage/Disk/DiskImage/Formats/SAP.cpp new file mode 100644 index 000000000..d14235fe5 --- /dev/null +++ b/Storage/Disk/DiskImage/Formats/SAP.cpp @@ -0,0 +1,40 @@ +// +// SAP.cpp +// Clock Signal +// +// Created by Thomas Harte on 20/04/2026. +// Copyright © 2026 Thomas Harte. All rights reserved. +// + +#include "SAP.hpp" + +using namespace Storage::Disk; + +SAP::SAP(const std::string &file_name) : file_(file_name) { +} + +HeadPosition SAP::maximum_head_position() const { + return HeadPosition(2); +} + +bool SAP::is_read_only() const { + return true; +} + +bool SAP::represents(const std::string &name) const { + return name == file_.name(); +} + +Track::Address SAP::canonical_address(const Track::Address address) const { + return Track::Address( + address.head, + HeadPosition(address.position.as_int()) + ); +} + +std::unique_ptr SAP::track_at_position(Track::Address) const { + return nullptr; +} + +void SAP::set_tracks(const std::map> &) { +} diff --git a/Storage/Disk/DiskImage/Formats/SAP.hpp b/Storage/Disk/DiskImage/Formats/SAP.hpp new file mode 100644 index 000000000..e02cbc415 --- /dev/null +++ b/Storage/Disk/DiskImage/Formats/SAP.hpp @@ -0,0 +1,33 @@ +// +// SAP.hpp +// Clock Signal +// +// Created by Thomas Harte on 20/04/2026. +// Copyright © 2026 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "Storage/Disk/DiskImage/DiskImage.hpp" +#include "Storage/Disk/Track/PCMTrack.hpp" +#include "Storage/FileHolder.hpp" +#include + +namespace Storage::Disk { + +class SAP: public DiskImage { +public: + SAP(const std::string &file_name); + + HeadPosition maximum_head_position() const; + Track::Address canonical_address(Track::Address) const; + std::unique_ptr track_at_position(Track::Address) const; + void set_tracks(const std::map> &tracks); + bool is_read_only() const; + bool represents(const std::string &) const; + +private: + mutable FileHolder file_; +}; + +} diff --git a/cmake/CLK_SOURCES.cmake b/cmake/CLK_SOURCES.cmake index 25a8cbf23..0a32638ce 100644 --- a/cmake/CLK_SOURCES.cmake +++ b/cmake/CLK_SOURCES.cmake @@ -220,6 +220,7 @@ set(CLK_SOURCES Storage/Disk/DiskImage/Formats/NIB.cpp Storage/Disk/DiskImage/Formats/OricMFMDSK.cpp Storage/Disk/DiskImage/Formats/PCBooter.cpp + Storage/Disk/DiskImage/Formats/SAP.cpp Storage/Disk/DiskImage/Formats/SSD.cpp Storage/Disk/DiskImage/Formats/STX.cpp Storage/Disk/DiskImage/Formats/Utility/ImplicitSectors.cpp From b65ce0f0cbb58000629e3d3ec9b7b9b5e10d6e3e Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 20 Apr 2026 11:48:45 -0400 Subject: [PATCH 2/4] Superficially appear to parse SAP files. --- Numeric/CRC.hpp | 4 --- Storage/Disk/DiskImage/Formats/SAP.cpp | 45 ++++++++++++++++++++++++++ Storage/Disk/DiskImage/Formats/SAP.hpp | 1 + 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/Numeric/CRC.hpp b/Numeric/CRC.hpp index 43f1c2718..80601a2d1 100644 --- a/Numeric/CRC.hpp +++ b/Numeric/CRC.hpp @@ -27,10 +27,6 @@ template < > class Generator { public: - /*! - Instantiates a CRC16 that will compute the CRC16 specified by the supplied - @c polynomial and @c reset_value. - */ constexpr Generator() noexcept: value_(reset_value) {} /// Resets the CRC to the reset value. diff --git a/Storage/Disk/DiskImage/Formats/SAP.cpp b/Storage/Disk/DiskImage/Formats/SAP.cpp index d14235fe5..9f59ba90e 100644 --- a/Storage/Disk/DiskImage/Formats/SAP.cpp +++ b/Storage/Disk/DiskImage/Formats/SAP.cpp @@ -8,9 +8,54 @@ #include "SAP.hpp" +#include "Numeric/CRC.hpp" + using namespace Storage::Disk; SAP::SAP(const std::string &file_name) : file_(file_name) { + // Header: + // + // 1 byte: disk geometry. + // format 0 => 80 tracks, 256-byte sectors, 16 sectors/track; + // format 1 => 40 tracks, 128-byte sectors, 16 sectors/track. + // 65 bytes: text signature. + // + sector_size_ = file_.get(); + if(sector_size_ != 1 && sector_size_ != 2) { + throw Error::InvalidFormat; + } + + // Convert to regularised IBM-style floppy disk form. + sector_size_ = sector_size_ == 2 ? 0 : 1; + + // Test only the start of the signature. + if(!file_.check_signature("SYSTEME D'ARCHIVAGE PUKALL S.A.P.")) { + throw Error::InvalidFormat; + } + file_.seek(66, Whence::SET); + + // Sectors from here: 4 bytes address, then all data, then a CRC16 of the data section. + // On-disk data bytes are XORd with 0xb3 for some reason. + while(true) { + const auto format = file_.get(); + const auto protection = file_.get(); + const auto track = file_.get(); + const auto sector = file_.get(); + if(file_.eof()) break; + printf("format %d protection %d track %d sector %d; ", format, protection, track, sector); + + file_.seek(-4, Whence::CUR); + auto contents = file_.read(256 + 4); + for(size_t c = 4; c < contents.size(); c++) { + contents[c] ^= 0xb3; + } + const auto calculated = CRC::Generator::crc_of(contents); + + const auto crc = file_.get_be(); + printf("CRC: %04x [%04x]\n", crc, calculated); + } + + printf("---\n"); } HeadPosition SAP::maximum_head_position() const { diff --git a/Storage/Disk/DiskImage/Formats/SAP.hpp b/Storage/Disk/DiskImage/Formats/SAP.hpp index e02cbc415..bd255ef41 100644 --- a/Storage/Disk/DiskImage/Formats/SAP.hpp +++ b/Storage/Disk/DiskImage/Formats/SAP.hpp @@ -28,6 +28,7 @@ public: private: mutable FileHolder file_; + uint8_t sector_size_; }; } From 236b8ed5a027f54d51e4acb8c5bc3f82eb8a1669 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 20 Apr 2026 12:03:32 -0400 Subject: [PATCH 3/4] Attempt full track reconstruction. --- Storage/Disk/DiskImage/Formats/SAP.cpp | 68 +++++++++++++++----------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/Storage/Disk/DiskImage/Formats/SAP.cpp b/Storage/Disk/DiskImage/Formats/SAP.cpp index 9f59ba90e..eb2150cb5 100644 --- a/Storage/Disk/DiskImage/Formats/SAP.cpp +++ b/Storage/Disk/DiskImage/Formats/SAP.cpp @@ -9,6 +9,9 @@ #include "SAP.hpp" #include "Numeric/CRC.hpp" +#include "Storage/Disk/Encodings/MFM/Encoder.hpp" + +#include using namespace Storage::Disk; @@ -32,34 +35,11 @@ SAP::SAP(const std::string &file_name) : file_(file_name) { if(!file_.check_signature("SYSTEME D'ARCHIVAGE PUKALL S.A.P.")) { throw Error::InvalidFormat; } - file_.seek(66, Whence::SET); - - // Sectors from here: 4 bytes address, then all data, then a CRC16 of the data section. - // On-disk data bytes are XORd with 0xb3 for some reason. - while(true) { - const auto format = file_.get(); - const auto protection = file_.get(); - const auto track = file_.get(); - const auto sector = file_.get(); - if(file_.eof()) break; - printf("format %d protection %d track %d sector %d; ", format, protection, track, sector); - - file_.seek(-4, Whence::CUR); - auto contents = file_.read(256 + 4); - for(size_t c = 4; c < contents.size(); c++) { - contents[c] ^= 0xb3; - } - const auto calculated = CRC::Generator::crc_of(contents); - - const auto crc = file_.get_be(); - printf("CRC: %04x [%04x]\n", crc, calculated); - } - - printf("---\n"); } HeadPosition SAP::maximum_head_position() const { - return HeadPosition(2); + // In the SAP file format, this is coupled to sector size. + return HeadPosition(sector_size_ ? 80 : 40); } bool SAP::is_read_only() const { @@ -77,9 +57,39 @@ Track::Address SAP::canonical_address(const Track::Address address) const { ); } -std::unique_ptr SAP::track_at_position(Track::Address) const { - return nullptr; +std::unique_ptr SAP::track_at_position(const Track::Address address) const { + static constexpr int sectors_per_track = 16; + const auto track_size = sectors_per_track * ((128 << sector_size_) + 6); + const auto header_size = 66; + file_.seek(address.position.as_int() * track_size + header_size, Whence::SET); + + std::vector sectors; + sectors.reserve(sectors_per_track); + + for(int s = 0; s < sectors_per_track; s++) { + auto §or = sectors.emplace_back(); + + [[maybe_unused]] const auto format = file_.get(); + [[maybe_unused]] const auto protection = file_.get(); + sector.address.track = file_.get(); + sector.address.sector = file_.get(); + sector.size = sector_size_; + + // SAP uses almost-but-not-quite an ordinary CCITT CRC. + file_.seek(-4, Whence::CUR); + auto contents = file_.read(256 + 4); + for(size_t c = 4; c < contents.size(); c++) { + contents[c] ^= 0xb3; + } + const auto calculated = CRC::Generator::crc_of(contents); + const auto crc = file_.get_be(); + + contents.erase(contents.begin(), contents.begin() + 4); + sector.samples.push_back(contents); + sector.has_data_crc_error = crc != calculated; + } + + return Encodings::MFM::TrackWithSectors(Encodings::MFM::Density::Double, sectors); } -void SAP::set_tracks(const std::map> &) { -} +void SAP::set_tracks(const std::map> &) {} From 71e53c1b4e3f3116809e9794584bb48bb21813b5 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 20 Apr 2026 12:06:12 -0400 Subject: [PATCH 4/4] Avoid out-of-bounds accesses. --- Storage/Disk/DiskImage/Formats/SAP.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Storage/Disk/DiskImage/Formats/SAP.cpp b/Storage/Disk/DiskImage/Formats/SAP.cpp index eb2150cb5..c9c9367f8 100644 --- a/Storage/Disk/DiskImage/Formats/SAP.cpp +++ b/Storage/Disk/DiskImage/Formats/SAP.cpp @@ -78,6 +78,9 @@ std::unique_ptr SAP::track_at_position(const Track::Address address) cons // SAP uses almost-but-not-quite an ordinary CCITT CRC. file_.seek(-4, Whence::CUR); auto contents = file_.read(256 + 4); + if(file_.eof()) break; + + // On-disk bytes are XORd with B3. For some reason. for(size_t c = 4; c < contents.size(); c++) { contents[c] ^= 0xb3; }