mirror of
https://github.com/TomHarte/CLK.git
synced 2024-09-07 12:54:34 +00:00
commit
dc0a82cf9a
@ -9,13 +9,29 @@
|
||||
#include "StaticAnalyser.hpp"
|
||||
#include "Target.hpp"
|
||||
|
||||
#include "../../../Storage/Disk/Parsers/FAT.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
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<Analyser::Static::Target>(target));
|
||||
|
@ -220,6 +220,12 @@ template <bool has_disk_controller> 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 <bool has_disk_controller> 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 <bool has_disk_controller> class ConcreteMachine:
|
||||
void type_string(const std::string &string) final {
|
||||
Utility::TypeRecipient<CharacterMapper>::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<CharacterMapper>::can_type(c);
|
||||
}
|
||||
|
||||
bool is_past_splash_screen_ = false;
|
||||
bool should_skip_splash_screen_ = false;
|
||||
int typer_delay_ = 30;
|
||||
|
||||
// MARK: - MediaTarget
|
||||
|
@ -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 = "<group>"; };
|
||||
4B4518A71F76004200926311 /* TapeParser.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = TapeParser.hpp; path = Parsers/TapeParser.hpp; sourceTree = "<group>"; };
|
||||
4B4518A81F76022000926311 /* DiskImageImplementation.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = DiskImageImplementation.hpp; sourceTree = "<group>"; };
|
||||
4B477709268FBE4D005C2340 /* FAT.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = FAT.cpp; path = Parsers/FAT.cpp; sourceTree = "<group>"; };
|
||||
4B47770A268FBE4D005C2340 /* FAT.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = FAT.hpp; path = Parsers/FAT.hpp; sourceTree = "<group>"; };
|
||||
4B47F6C4241C87A100ED06F7 /* Struct.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Struct.cpp; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
@ -2657,7 +2660,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4B3FE75C1F3CF68B00448EE4 /* CPM.cpp */,
|
||||
4B477709268FBE4D005C2340 /* FAT.cpp */,
|
||||
4B3FE75D1F3CF68B00448EE4 /* CPM.hpp */,
|
||||
4B47770A268FBE4D005C2340 /* FAT.hpp */,
|
||||
);
|
||||
name = Parsers;
|
||||
sourceTree = "<group>";
|
||||
@ -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 */,
|
||||
|
179
Storage/Disk/Parsers/FAT.cpp
Normal file
179
Storage/Disk/Parsers/FAT.cpp
Normal file
@ -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 <iostream>
|
||||
|
||||
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 <typename CharT> 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<uint8_t> &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::Volume> FAT::GetVolume(const std::shared_ptr<Storage::Disk::Disk> &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<uint8_t> 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<uint8_t> 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<std::vector<uint8_t>> FAT::GetFile(const std::shared_ptr<Storage::Disk::Disk> &disk, const Volume &volume, const File &file) {
|
||||
Storage::Encodings::MFM::Parser parser(true, disk);
|
||||
|
||||
std::vector<uint8_t> 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::Directory> FAT::GetDirectory(const std::shared_ptr<Storage::Disk::Disk> &disk, const Volume &volume, const File &file) {
|
||||
const auto contents = GetFile(disk, volume, file);
|
||||
if(!contents) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return directory_from(*contents);
|
||||
}
|
79
Storage/Disk/Parsers/FAT.hpp
Normal file
79
Storage/Disk/Parsers/FAT.hpp
Normal file
@ -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 <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<File>;
|
||||
|
||||
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<uint16_t> 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<Volume> GetVolume(const std::shared_ptr<Storage::Disk::Disk> &disk);
|
||||
std::optional<std::vector<uint8_t>> GetFile(const std::shared_ptr<Storage::Disk::Disk> &disk, const Volume &volume, const File &file);
|
||||
std::optional<Directory> GetDirectory(const std::shared_ptr<Storage::Disk::Disk> &disk, const Volume &volume, const File &file);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* FAT_hpp */
|
Loading…
Reference in New Issue
Block a user