diff --git a/Analyser/Static/Enterprise/StaticAnalyser.cpp b/Analyser/Static/Enterprise/StaticAnalyser.cpp index 7b701b1d9..e4ff7ef8e 100644 --- a/Analyser/Static/Enterprise/StaticAnalyser.cpp +++ b/Analyser/Static/Enterprise/StaticAnalyser.cpp @@ -9,13 +9,29 @@ #include "StaticAnalyser.hpp" #include "Target.hpp" +#include "../../../Storage/Disk/Parsers/FAT.hpp" + +#include + +namespace { + +bool insensitive_equal(const std::string &lhs, const std::string &rhs) { + return std::equal( + lhs.begin(), lhs.end(), + rhs.begin(), rhs.end(), + [] (char l, char r) { + return tolower(l) == tolower(r); + }); +} + +} + Analyser::Static::TargetList Analyser::Static::Enterprise::GetTargets(const Media &media, const std::string &, TargetPlatform::IntType) { // This analyser can comprehend disks only. if(media.disks.empty()) return {}; - // Otherwise, for now: wave it through. + // Otherwise, assume a return will happen. Analyser::Static::TargetList targets; - using Target = Analyser::Static::Enterprise::Target; auto *const target = new Target; target->media = media; @@ -23,9 +39,40 @@ Analyser::Static::TargetList Analyser::Static::Enterprise::GetTargets(const Medi // Always require a BASIC. target->basic_version = Target::BASICVersion::Any; - // If this is a single-sided floppy disk, guess the Macintosh 512kb. + // Inspect any supplied disks. if(!media.disks.empty()) { + // DOS will be needed. target->dos = Target::DOS::EXDOS; + + // Grab the volume information, which includes the root directory. + auto volume = Storage::Disk::FAT::GetVolume(media.disks.front()); + if(volume) { + // If there's an EXDOS.INI then this disk should be able to boot itself. + // If not but if there's only one .COM or .BAS, automatically load that. + // Failing that, issue a :DIR and give the user a clue as to how to load. + const Storage::Disk::FAT::File *selected_file = nullptr; + bool has_exdos_ini = false; + bool did_pick_file = false; + for(const auto &file: (*volume).root_directory) { + if(insensitive_equal(file.name, "exdos") && insensitive_equal(file.extension, "ini")) { + has_exdos_ini = true; + break; + } + + if(insensitive_equal(file.extension, "com") || insensitive_equal(file.extension, "bas")) { + did_pick_file = !selected_file; + selected_file = &file; + } + } + + if(!has_exdos_ini) { + if(did_pick_file) { + target->loading_command = std::string("run \"") + selected_file->name + "." + selected_file->extension + "\""; + } else { + target->loading_command = ":dir\n"; + } + } + } } targets.push_back(std::unique_ptr(target)); diff --git a/Machines/Enterprise/Enterprise.cpp b/Machines/Enterprise/Enterprise.cpp index 47da8f8e2..e1cc9b86c 100644 --- a/Machines/Enterprise/Enterprise.cpp +++ b/Machines/Enterprise/Enterprise.cpp @@ -220,6 +220,12 @@ template class ConcreteMachine: if(!target.loading_command.empty()) { type_string(target.loading_command); } + + // Ensure the splash screen is automatically skipped if any media has been provided. + if(!target.media.empty()) { + should_skip_splash_screen_ = !target.media.empty(); + typer_delay_ = 2; + } } ~ConcreteMachine() { @@ -438,18 +444,18 @@ template class ConcreteMachine: // spot that a scan of the keyboard just finished. Which makes it // time to enqueue the next keypress. // - // Re: is_past_splash_screen_ and typer_delay_, assume that a + // Re: should_skip_splash_screen_ and typer_delay_, assume that a // single keypress is necessary to get past the Enterprise splash // screen, then a pause in keypressing while BASIC or whatever // starts up, then presses can resume. - if(typer_ && active_key_line_ == 9 && !(*cycle.value & 0xf)) { - if(!is_past_splash_screen_) { + if(active_key_line_ == 9 && !(*cycle.value & 0xf) && (should_skip_splash_screen_ || typer_)) { + if(should_skip_splash_screen_) { set_key_state(uint16_t(Key::Space), typer_delay_); if(typer_delay_) { --typer_delay_; } else { typer_delay_ = 60; - is_past_splash_screen_ = true; + should_skip_splash_screen_ = false; } } else { if(!typer_delay_) { @@ -620,15 +626,20 @@ template class ConcreteMachine: void type_string(const std::string &string) final { Utility::TypeRecipient::add_typer(string); - is_past_splash_screen_ = !z80_.get_is_resetting(); - typer_delay_ = !is_past_splash_screen_; + if(z80_.get_is_resetting()) { + should_skip_splash_screen_ = true; + typer_delay_ = 1; + } else { + should_skip_splash_screen_ = false; + typer_delay_ = 0; + } } bool can_type(char c) const final { return Utility::TypeRecipient::can_type(c); } - bool is_past_splash_screen_ = false; + bool should_skip_splash_screen_ = false; int typer_delay_ = 30; // MARK: - MediaTarget diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 8587d4e08..f2b162d23 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -241,6 +241,7 @@ 4B4518A31F75FD1C00926311 /* HFE.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4518951F75FD1B00926311 /* HFE.cpp */; }; 4B4518A41F75FD1C00926311 /* OricMFMDSK.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4518971F75FD1B00926311 /* OricMFMDSK.cpp */; }; 4B4518A51F75FD1C00926311 /* SSD.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4518991F75FD1B00926311 /* SSD.cpp */; }; + 4B47770B268FBE4D005C2340 /* FAT.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B477709268FBE4D005C2340 /* FAT.cpp */; }; 4B47F6C5241C87A100ED06F7 /* Struct.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B47F6C4241C87A100ED06F7 /* Struct.cpp */; }; 4B47F6C6241C87A100ED06F7 /* Struct.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B47F6C4241C87A100ED06F7 /* Struct.cpp */; }; 4B49F0A923346F7A0045E6A6 /* MacintoshOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B49F0A723346F7A0045E6A6 /* MacintoshOptions.xib */; }; @@ -1276,6 +1277,8 @@ 4B45189A1F75FD1B00926311 /* SSD.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SSD.hpp; sourceTree = ""; }; 4B4518A71F76004200926311 /* TapeParser.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = TapeParser.hpp; path = Parsers/TapeParser.hpp; sourceTree = ""; }; 4B4518A81F76022000926311 /* DiskImageImplementation.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = DiskImageImplementation.hpp; sourceTree = ""; }; + 4B477709268FBE4D005C2340 /* FAT.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = FAT.cpp; path = Parsers/FAT.cpp; sourceTree = ""; }; + 4B47770A268FBE4D005C2340 /* FAT.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = FAT.hpp; path = Parsers/FAT.hpp; sourceTree = ""; }; 4B47F6C4241C87A100ED06F7 /* Struct.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Struct.cpp; sourceTree = ""; }; 4B49F0A823346F7A0045E6A6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/MacintoshOptions.xib"; sourceTree = SOURCE_ROOT; }; 4B4A762E1DB1A3FA007AAE2E /* AY38910.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AY38910.cpp; sourceTree = ""; }; @@ -2657,7 +2660,9 @@ isa = PBXGroup; children = ( 4B3FE75C1F3CF68B00448EE4 /* CPM.cpp */, + 4B477709268FBE4D005C2340 /* FAT.cpp */, 4B3FE75D1F3CF68B00448EE4 /* CPM.hpp */, + 4B47770A268FBE4D005C2340 /* FAT.hpp */, ); name = Parsers; sourceTree = ""; @@ -5579,6 +5584,7 @@ 4B69FB3D1C4D908A00B5F0AA /* Tape.cpp in Sources */, 4B4518841F75E91A00926311 /* UnformattedTrack.cpp in Sources */, 4B65086022F4CF8D009C1100 /* Keyboard.cpp in Sources */, + 4B47770B268FBE4D005C2340 /* FAT.cpp in Sources */, 4B894528201967B4007DE474 /* Disk.cpp in Sources */, 4B2E86CF25D8D8C70024F1E9 /* Keyboard.cpp in Sources */, 4BBB70A4202011C2002FE009 /* MultiMediaTarget.cpp in Sources */, diff --git a/Storage/Disk/Parsers/FAT.cpp b/Storage/Disk/Parsers/FAT.cpp new file mode 100644 index 000000000..528c73248 --- /dev/null +++ b/Storage/Disk/Parsers/FAT.cpp @@ -0,0 +1,179 @@ +// +// FAT.cpp +// Clock Signal +// +// Created by Thomas Harte on 02/07/2021. +// Copyright © 2021 Thomas Harte. All rights reserved. +// + +#include "FAT.hpp" + +#include "../Encodings/MFM/Parser.hpp" + +#include + +using namespace Storage::Disk; + +FAT::Volume::CHS FAT::Volume::chs_for_sector(int sector) const { + const auto track = sector / sectors_per_track; + + // Sides are interleaved. + return CHS{ + track / head_count, + track % head_count, + 1 + (sector % sectors_per_track) + }; +} + +int FAT::Volume::sector_for_cluster(uint16_t cluster) const { + // The first cluster in the data area is numbered as 2. + return ((cluster - 2) * sectors_per_cluster) + first_data_sector; +} + +namespace { + +template std::string trim(CharT start, CharT end) { + std::string result(start, end); + result.erase(std::find_if(result.rbegin(), result.rend(), [](unsigned char ch) { + return !std::isspace(ch); + }).base(), result.end()); + return result; +} + +FAT::Directory directory_from(const std::vector &contents) { + FAT::Directory result; + + // Worst case: parse until the amount of data supplied is fully consumed. + for(size_t base = 0; base < contents.size(); base += 32) { + // An entry starting with byte 0 indicates end-of-directory. + if(!contents[base]) { + break; + } + + // An entry starting in 0xe5 is merely deleted. + if(contents[base] == 0xe5) { + continue; + } + + // Otherwise create and populate a new entry. + result.emplace_back(); + result.back().name = trim(&contents[base], &contents[base+8]); + result.back().extension = trim(&contents[base+8], &contents[base+11]); + result.back().attributes = contents[base + 11]; + result.back().time = uint16_t(contents[base+22] | (contents[base+23] << 8)); + result.back().date = uint16_t(contents[base+24] | (contents[base+25] << 8)); + result.back().starting_cluster = uint16_t(contents[base+26] | (contents[base+27] << 8)); + result.back().size = uint32_t( + contents[base+28] | + (contents[base+29] << 8) | + (contents[base+30] << 16) | + (contents[base+31] << 24) + ); + } + + return result; +} + +} + +std::optional FAT::GetVolume(const std::shared_ptr &disk) { + Storage::Encodings::MFM::Parser parser(true, disk); + + // Grab the boot sector; that'll be enough to establish the volume. + Storage::Encodings::MFM::Sector *const boot_sector = parser.get_sector(0, 0, 1); + if(!boot_sector || boot_sector->samples.empty() || boot_sector->samples[0].size() < 512) { + return std::nullopt; + } + + // Obtain volume details. + const auto &data = boot_sector->samples[0]; + FAT::Volume volume; + volume.bytes_per_sector = uint16_t(data[11] | (data[12] << 8)); + volume.sectors_per_cluster = data[13]; + volume.reserved_sectors = uint16_t(data[14] | (data[15] << 8)); + volume.fat_copies = data[16]; + const uint16_t root_directory_entries = uint16_t(data[17] | (data[18] << 8)); + volume.total_sectors = uint16_t(data[19] | (data[20] << 8)); + volume.sectors_per_fat = uint16_t(data[22] | (data[23] << 8)); + volume.sectors_per_track = uint16_t(data[24] | (data[25] << 8)); + volume.head_count = uint16_t(data[26] | (data[27] << 8)); + volume.correct_signature = data[510] == 0x55 && data[511] == 0xaa; + + const size_t root_directory_sectors = (root_directory_entries*32 + volume.bytes_per_sector - 1) / volume.bytes_per_sector; + volume.first_data_sector = int(volume.reserved_sectors + volume.sectors_per_fat*volume.fat_copies + root_directory_sectors); + + // Grab the FAT. + std::vector source_fat; + for(int c = 0; c < volume.sectors_per_fat; c++) { + const int sector_number = volume.reserved_sectors + c; + const auto address = volume.chs_for_sector(sector_number); + + Storage::Encodings::MFM::Sector *const fat_sector = + parser.get_sector(address.head, address.cylinder, uint8_t(address.sector)); + if(!fat_sector || fat_sector->samples.empty() || fat_sector->samples[0].size() != volume.bytes_per_sector) { + return std::nullopt; + } + std::copy(fat_sector->samples[0].begin(), fat_sector->samples[0].end(), std::back_inserter(source_fat)); + } + + // Decode the FAT. + // TODO: stop assuming FAT12 here. + for(size_t c = 0; c < source_fat.size(); c += 3) { + const uint32_t double_cluster = uint32_t(source_fat[c] + (source_fat[c + 1] << 8) + (source_fat[c + 2] << 16)); + volume.fat.push_back(uint16_t(double_cluster & 0xfff)); + volume.fat.push_back(uint16_t(double_cluster >> 12)); + } + + // Grab the root directory. + std::vector root_directory; + for(size_t c = 0; c < root_directory_sectors; c++) { + const auto sector_number = int(volume.reserved_sectors + c + volume.sectors_per_fat*volume.fat_copies); + const auto address = volume.chs_for_sector(sector_number); + + Storage::Encodings::MFM::Sector *const sector = + parser.get_sector(address.head, address.cylinder, uint8_t(address.sector)); + if(!sector || sector->samples.empty() || sector->samples[0].size() != volume.bytes_per_sector) { + return std::nullopt; + } + std::copy(sector->samples[0].begin(), sector->samples[0].end(), std::back_inserter(root_directory)); + } + volume.root_directory = directory_from(root_directory); + + return volume; +} + +std::optional> FAT::GetFile(const std::shared_ptr &disk, const Volume &volume, const File &file) { + Storage::Encodings::MFM::Parser parser(true, disk); + + std::vector contents; + + // In FAT cluster numbers describe a linked list via the FAT table, with values above $FF0 being reserved + // (relevantly: FF7 means bad cluster; FF8–FFF mean end-of-file). + uint16_t cluster = file.starting_cluster; + do { + const int sector = volume.sector_for_cluster(cluster); + + for(int c = 0; c < volume.sectors_per_cluster; c++) { + const auto address = volume.chs_for_sector(sector + c); + + Storage::Encodings::MFM::Sector *const sector_contents = + parser.get_sector(address.head, address.cylinder, uint8_t(address.sector)); + if(!sector_contents || sector_contents->samples.empty() || sector_contents->samples[0].size() != volume.bytes_per_sector) { + return std::nullopt; + } + std::copy(sector_contents->samples[0].begin(), sector_contents->samples[0].end(), std::back_inserter(contents)); + } + + cluster = volume.fat[cluster]; + } while(cluster < 0xff0); + + return contents; +} + +std::optional FAT::GetDirectory(const std::shared_ptr &disk, const Volume &volume, const File &file) { + const auto contents = GetFile(disk, volume, file); + if(!contents) { + return std::nullopt; + } + return directory_from(*contents); +} diff --git a/Storage/Disk/Parsers/FAT.hpp b/Storage/Disk/Parsers/FAT.hpp new file mode 100644 index 000000000..3ede18518 --- /dev/null +++ b/Storage/Disk/Parsers/FAT.hpp @@ -0,0 +1,79 @@ +// +// FAT.hpp +// Clock Signal +// +// Created by Thomas Harte on 02/07/2021. +// Copyright © 2021 Thomas Harte. All rights reserved. +// + +#ifndef Storage_Disk_Parsers_FAT_hpp +#define Storage_Disk_Parsers_FAT_hpp + +#include "../Disk.hpp" + +#include +#include +#include +#include + +namespace Storage { +namespace Disk { +namespace FAT { + +struct File { + std::string name; + std::string extension; + uint8_t attributes = 0; + uint16_t time = 0; // TODO: offer time/date decoders. + uint16_t date = 0; + uint16_t starting_cluster = 0; + uint32_t size = 0; + + enum Attribute: uint8_t { + ReadOnly = (1 << 0), + Hidden = (1 << 1), + System = (1 << 2), + VolumeLabel = (1 << 3), + Directory = (1 << 4), + Archive = (1 << 5), + }; +}; + +using Directory = std::vector; + +struct Volume { + uint16_t bytes_per_sector = 0; + uint8_t sectors_per_cluster = 0; + uint16_t reserved_sectors = 0; + uint8_t fat_copies = 0; + uint16_t total_sectors = 0; + uint16_t sectors_per_fat = 0; + uint16_t sectors_per_track = 0; + uint16_t head_count = 0; + uint16_t hidden_sectors = 0; + bool correct_signature = false; + int first_data_sector = 0; + + std::vector fat; + Directory root_directory; + + struct CHS { + int cylinder; + int head; + int sector; + }; + /// @returns a direct sector -> CHS address translation. + CHS chs_for_sector(int sector) const; + /// @returns the CHS address for the numbered cluster within the data area. + int sector_for_cluster(uint16_t cluster) const; +}; + +std::optional GetVolume(const std::shared_ptr &disk); +std::optional> GetFile(const std::shared_ptr &disk, const Volume &volume, const File &file); +std::optional GetDirectory(const std::shared_ptr &disk, const Volume &volume, const File &file); + +} +} +} + +#endif /* FAT_hpp */