1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-11-23 05:19:20 +00:00

Merge pull request #1643 from TomHarte/EXOSDirect

Enterprise: support .BAS and .COM and corresponding host filing system access.
This commit is contained in:
Thomas Harte
2025-11-22 12:38:33 -05:00
committed by GitHub
18 changed files with 1028 additions and 20 deletions

View File

@@ -33,7 +33,7 @@ Analyser::Static::TargetList Analyser::Static::Enterprise::GetTargets(
bool
) {
// This analyser can comprehend disks only.
if(media.disks.empty()) return {};
if(media.disks.empty() && media.file_bundles.empty()) return {};
// Otherwise, assume a return will happen.
Analyser::Static::TargetList targets;
@@ -86,7 +86,36 @@ Analyser::Static::TargetList Analyser::Static::Enterprise::GetTargets(
}
}
targets.push_back(std::unique_ptr<Analyser::Static::Target>(target));
if(!media.file_bundles.empty()) {
auto &bundle = *media.file_bundles.front();
const auto key = bundle.key_file();
if(key.has_value()) {
auto file = bundle.open(*key, Storage::FileMode::Read);
enum class FileType: uint16_t {
COM = 0x0500,
BAS = 0x0400,
};
// Check for a .COM by inspecting the header.
const auto type = FileType(file.get_le<uint16_t>());
const uint16_t size = file.get_le<uint16_t>();
// There are then 12 bytes of 0 padding that could be tested for.
if((type != FileType::COM && type != FileType::BAS) || size > file.stats().st_size - 16) {
target->media.file_bundles.clear();
} else {
target->loading_command = "run \"file:\"\n";
}
}
// TODO: look for a key file, similar logic to above.
}
if(!target->media.empty()) {
targets.push_back(std::unique_ptr<Analyser::Static::Target>(target));
}
return targets;
}

View File

@@ -63,6 +63,9 @@
#include "Storage/Disk/DiskImage/Formats/STX.hpp"
#include "Storage/Disk/DiskImage/Formats/WOZ.hpp"
// File Bundles.
#include "Storage/FileBundle/FileBundle.hpp"
// Mass Storage Devices (i.e. usually, hard disks)
#include "Storage/MassStorage/Formats/DAT.hpp"
#include "Storage/MassStorage/Formats/DSK.hpp"
@@ -123,6 +126,8 @@ public:
media.cartridges.push_back(instance);
} else if constexpr (std::is_base_of_v<Storage::MassStorage::MassStorageDevice, InstanceT>) {
media.mass_storage_devices.push_back(instance);
} else if constexpr (std::is_base_of_v<Storage::FileBundle::FileBundle, InstanceT>) {
media.file_bundles.push_back(instance);
} else {
static_assert(always_false_v<InstanceT>, "Unexpected type encountered.");
}
@@ -208,13 +213,14 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
accumulator.try_standard<Disk::DiskImageHolder<Disk::AcornADF>>(TargetPlatform::Acorn, "adf");
accumulator.try_standard<Disk::DiskImageHolder<Disk::AmigaADF>>(TargetPlatform::Amiga, "adf");
accumulator.try_standard<Disk::DiskImageHolder<Disk::AcornADF>>(TargetPlatform::Acorn, "adl");
accumulator.try_standard<Disk::DiskImageHolder<Disk::JFD>>(TargetPlatform::Archimedes, "jfd");
accumulator.try_standard<FileBundle::LocalFSFileBundle>(TargetPlatform::Enterprise, "bas");
accumulator.try_standard<Cartridge::BinaryDump>(TargetPlatform::AllCartridge, "bin");
accumulator.try_standard<Tape::CAS>(TargetPlatform::MSX, "cas");
accumulator.try_standard<Tape::TZX>(TargetPlatform::AmstradCPC, "cdt");
accumulator.try_standard<Cartridge::BinaryDump>(TargetPlatform::Coleco, "col");
accumulator.try_standard<FileBundle::LocalFSFileBundle>(TargetPlatform::Enterprise, "com");
accumulator.try_standard<Tape::CSW>(TargetPlatform::AllTape, "csw");
accumulator.try_standard<Disk::DiskImageHolder<Disk::D64>>(TargetPlatform::Commodore8bit, "d64");
@@ -257,6 +263,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
TargetPlatform::Amiga | TargetPlatform::AtariST | TargetPlatform::AmstradCPC | TargetPlatform::ZXSpectrum,
"ipf");
accumulator.try_standard<Disk::DiskImageHolder<Disk::JFD>>(TargetPlatform::Archimedes, "jfd");
accumulator.try_standard<Disk::DiskImageHolder<Disk::MSA>>(TargetPlatform::AtariST, "msa");
accumulator.try_standard<Cartridge::BinaryDump>(TargetPlatform::MSX, "mx2");
accumulator.try_standard<Disk::DiskImageHolder<Disk::NIB>>(TargetPlatform::DiskII, "nib");

View File

@@ -12,6 +12,7 @@
#include "Storage/Cartridge/Cartridge.hpp"
#include "Storage/Disk/Disk.hpp"
#include "Storage/FileBundle/FileBundle.hpp"
#include "Storage/MassStorage/MassStorageDevice.hpp"
#include "Storage/Tape/Tape.hpp"
#include "Storage/TargetPlatforms.hpp"
@@ -33,9 +34,15 @@ struct Media {
std::vector<std::shared_ptr<Storage::Tape::Tape>> tapes;
std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> cartridges;
std::vector<std::shared_ptr<Storage::MassStorage::MassStorageDevice>> mass_storage_devices;
std::vector<std::shared_ptr<Storage::FileBundle::FileBundle>> file_bundles;
bool empty() const {
return disks.empty() && tapes.empty() && cartridges.empty() && mass_storage_devices.empty();
return
disks.empty() &&
tapes.empty() &&
cartridges.empty() &&
mass_storage_devices.empty() &&
file_bundles.empty();
}
Media &operator +=(const Media &rhs) {
@@ -47,6 +54,7 @@ struct Media {
append(tapes, rhs.tapes);
append(cartridges, rhs.cartridges);
append(mass_storage_devices, rhs.mass_storage_devices);
append(file_bundles, rhs.file_bundles);
return *this;
}

View File

@@ -0,0 +1,159 @@
//
// EXOSCodes.hpp
// Clock Signal
//
// Created by Thomas Harte on 20/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
// Various EXOS codes, transcribed from EXOS20_technical_information.pdf via archive.org,
// which appears to be a compilation of original documentation so page numbers below
// refer to the page within the PDF. Numbers printed on the in-document pages are inconsistent.
namespace Enterprise::EXOS {
// Page 67.
enum class Function: uint8_t {
ResetSystem = 0, // RESET
OpenChannel = 1, // OPEN
CreateChannel = 2, // CREAT
CloseChannel = 3, // CLOSE
DestroyChannel = 4, // DEST
ReadCharacter = 5, // RDCH
ReadBlock = 6, // RDBLK
WriteCharacter = 7, // WRCH
WriteBlock = 8, // WRBLK
ReadChannelStatus = 9, // RSTAT
SetChannelStatus = 10, // SSTAT
SpecialFunction = 11, // SFUNC
SetReadToggleEXOSVariable = 16, // EVAR
CaptureChannel = 17, // CAPT
RedirectChannel = 18, // REDIR
SetDefaultDevice = 19, // DDEV
ReturnSystemStatus = 20, // SYSS
LinkDevices = 21, // LINK
ReadEXOSBoundary = 22, // READB
SetUSERBoundary = 23, // SETB,
AllocateSegment = 24, // ALLOC,
FreeSegment = 25, // FREE
LocateROMs = 26, // ROMS
AllocateChannelBuffer = 27, // BUFF
ReturnErrorMessage = 28, // ERRMSG
};
// Page 25.
enum class DeviceDescriptorFunction: uint8_t {
//
// Codes are the same as `Function` in the range 111.
//
Interrupt = 0,
Initialise = 12,
BufferMoved = 13,
};
enum class Error: uint8_t {
NoError = 0x00,
//
// General Kernel Errors.
//
InvalidFunctionCode = 0xff, // IFUNC
FunctionCallNotAllowed = 0xfe, // ILLFN
InvalidString = 0xfd, // INAME
InsufficientStack = 0xfc, // STACK
ChannelIllegalOrDoesNotExist = 0xfb, // ICHAN
DeviceDoesNotExist = 0xfa, // NODEV
ChannelAlreadyExists = 0xf9, // CHANX
NoAllocateBufferCallMade = 0xf8, // NOBUF
InsufficientRAMForBuffer = 0xf7, // NORAM
InsufficientVideoRAM = 0xf6, // NOVID
NoFreeSegments = 0xf5, // NOSEG
InvalidSegment = 0xf4, // ISEG
InvalidUserBoundary = 0xf3, // IBOUND
InvalidEXOSVariableNumber = 0xf2, // IVAR
InvalidDesviceDescriptorType = 0xf1, // IDESC
UnrecognisedCommandString = 0xf0, // NOSTR
InvalidFileHeader = 0xef, // ASCII
UnknownModuleType = 0xee, // ITYPE
InvalidRelocatableModule = 0xed, // IREL
NoModule = 0xec, // NOMOD
InvalidTimeOrDateValue, // ITIME
//
// General Device Errors.
//
InvalidSpecialFunctionCode = 0xea, // ISPEC
AttemptToOpenSecondChannel = 0xe9, // 2NDCH
InvalidUnitNumber = 0xe8, // IUNIT
FunctionNotSupported = 0xe7, // NOFN
InvalidEscapeSequence = 0xe6, // ESC
StopKeyPressed = 0xe5, // STOP
EndOfFileMetInRead = 0xe4, // EOF
ProtectionViolation = 0xe3, // PROT
//
// Device-Specific Errors.
//
// FileDoesNotExist = 0xea, // NOFIL
// FileAlreadyExists = 0xe9, // EXFIL
// FileAlreadyOpen = 0xe8, // FOPEN
// FileIsTooBig = 0xe6, // FSIZE
// InvalidFilePointerValue = 0xe5, // FPTR
//
// //
// // Keyboard errors.
// //
// InvalidFunctionKeyNumber = 0xe3, // KFKEY
// RunOutOfFunctionKeySpace = 0xe2, // KFSPC
//
// //
// // Sound errors.
// //
// EnvelopeInvalidOrTooBig = 0xe1, // SENV
// NotEnoughRoomToDefineEnvelope = 0xe0, // SENDBF
// EnvelopeStorageRequestedTooSmall = 0xdf, // SENFLO
// SoundQueueFull = 0xde, // SQFUL
//
// //
// // Video errors.
// //
// InvalidRowNumberToScroll = 0xdd, // VROW
// AttemptToMoveCursorOffPage = 0xdc, // VCURS
// InvalidColourPassedToINKOrPAPER = 0xdb, // VCOLR
// InvalidXOrYSizeToOPEN = 0xda, // VSIZE
// InvalidVideoModeToOPEN = 0xd9, // VMODE
// BadParameterToDISPLAY = 0xdb, // VDISP, and officially 'naff' rather than 'bad'
// NotEnoughRowsInPageToDISPLAY = 0xd7, // VDSP2
// AttemptToMoveBeamOffPage = 0xd6, // VBEAM
// LineStyleTooBig = 0xd5, // VLSTY
// LineModeTooBig = 0xd4, // VLMOD
// CantDisplayCharacterOrGraphic = 0xd3, // VCHAR
//
// //
// // Serial errors.
// //
// InvalidBaudRate = 0xd2, // BAUD
//
// //
// // Editor errors.
// //
// InvalidVideoPageForOPEN = 0xd1, // EVID
// TroubleInCommunicatingWithKeyboard = 0xd0, // EKEY
// InvalidCoordinatesForPosition = 0xcf, // ECURS
//
// //
// // Cassette errors.
// //
// CRCErrorFromCassetteDriver = 0xce, // CCRC
//
// //
// // Network errors
// //
// SerialDeviceOpenCannotUseNetwork = 0xcd, // SEROP
// ADDR_NETNotSetUp = 0xcc, // NOADR
};
}

View File

@@ -10,6 +10,7 @@
#include "Dave.hpp"
#include "EXDos.hpp"
#include "HostFSHandler.hpp"
#include "Keyboard.hpp"
#include "Nick.hpp"
@@ -23,6 +24,8 @@
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "Processors/Z80/Z80.hpp"
#include <unordered_set>
namespace {
using Logger = Log::Logger<Log::Source::Enterprise>;
}
@@ -72,6 +75,7 @@ template <bool has_disk_controller, bool is_6mhz> class ConcreteMachine:
public Activity::Source,
public Configurable::Device,
public CPU::Z80::BusHandler,
public HostFSHandler::MemoryAccessor,
public Machine,
public MachineTypes::AudioProducer,
public MachineTypes::MappedKeyboardMachine,
@@ -104,7 +108,8 @@ public:
z80_(*this),
nick_(ram_.end() - 65536),
dave_audio_(audio_queue_),
speaker_(dave_audio_) {
speaker_(dave_audio_),
host_fs_(*this) {
// Request a clock of 4Mhz; this'll be mapped upwards for Nick and downwards for Dave elsewhere.
set_clock_rate(clock_rate);
@@ -228,6 +233,14 @@ public:
memcpy(exdos_rom_.data(), exdos->second.data(), std::min(exdos_rom_.size(), exdos->second.size()));
}
// Possibly install the host FS ROM.
host_fs_rom_.fill(0xff);
if(!target.media.file_bundles.empty()) {
const auto rom = host_fs_.rom();
std::copy(rom.begin(), rom.end(), host_fs_rom_.begin());
find_host_fs_hooks();
}
// Seed key state.
clear_all_keys();
@@ -539,8 +552,40 @@ public:
}
break;
case PartialMachineCycle::Read:
case PartialMachineCycle::ReadOpcode:
{
static bool print_opcode = false;
if(print_opcode) {
printf("%04x: %02x\n", address, read_pointers_[address >> 14][address]);
}
}
// Potential segue for the host FS. I'm relying on branch prediction to
// avoid this cost almost always.
if(test_host_fs_traps_ && (address >> 14) == 3) [[unlikely]] {
const auto is_trap = host_fs_traps_.contains(address);
if(is_trap) {
using Register = CPU::Z80::Register;
uint8_t a = uint8_t(z80_.value_of(Register::A));
uint16_t bc = z80_.value_of(Register::BC);
uint16_t de = z80_.value_of(Register::DE);
// Grab function code from where the PC actually is, and return a NOP
host_fs_.perform(read_pointers_[address >> 14][address], a, bc, de);
*cycle.value = 0x00; // i.e. NOP.
z80_.set_value_of(Register::A, a);
z80_.set_value_of(Register::BC, bc);
z80_.set_value_of(Register::DE, de);
break;
}
}
[[fallthrough]];
case PartialMachineCycle::Read:
if(read_pointers_[address >> 14]) {
*cycle.value = read_pointers_[address >> 14][address];
} else {
@@ -570,13 +615,23 @@ public:
private:
// MARK: - Memory layout
std::array<uint8_t, 256 * 1024> ram_{};
std::array<uint8_t, 64 * 1024> exos_;
std::array<uint8_t, 16 * 1024> basic_;
std::array<uint8_t, 16 * 1024> exdos_rom_;
std::array<uint8_t, 32 * 1024> epdos_rom_;
std::array<uint8_t, 16 * 1024> host_fs_rom_;
const uint8_t min_ram_slot_;
uint8_t *ram_segment(const uint8_t page) {
if(page < min_ram_slot_) return nullptr;
const auto ram_floor = (0x100 << 14) - ram_.size();
// Each segment is 2^14 bytes long and there are 256 of them. So the Enterprise has a 22-bit address space.
// RAM is at the end of that range; `ram_floor` is the 22-bit address at which RAM starts.
return &ram_[size_t((page << 14)) - ram_floor];
}
const uint8_t *read_pointers_[4] = {nullptr, nullptr, nullptr, nullptr};
uint8_t *write_pointers_[4] = {nullptr, nullptr, nullptr, nullptr};
uint8_t pages_[4] = {0x80, 0x80, 0x80, 0x80};
@@ -595,20 +650,29 @@ private:
template <size_t slot> void page(const uint8_t offset) {
pages_[slot] = offset;
if constexpr (slot == 3) {
test_host_fs_traps_ = false;
}
if(page_rom<slot>(offset, 0, exos_)) return;
if(page_rom<slot>(offset, 16, basic_)) return;
if(page_rom<slot>(offset, 32, exdos_rom_)) return;
if(page_rom<slot>(offset, 48, epdos_rom_)) return;
if(page_rom<slot>(offset, 64, host_fs_rom_)) {
if constexpr (slot == 3) {
test_host_fs_traps_ = true;
}
return;
}
// Of whatever size of RAM I've declared above, use only the final portion.
// This correlated with Nick always having been handed the final 64kb and,
// at least while the RAM is the first thing declared above, does a little
// to benefit data locality. Albeit not in a useful sense.
if(offset >= min_ram_slot_) {
const auto ram_floor = 4194304 - ram_.size();
const size_t address = offset * 0x4000 - ram_floor;
is_video_[slot] = offset >= 0xfc; // TODO: this hard-codes a 64kb video assumption.
page<slot>(&ram_[address], &ram_[address]);
auto pointer = ram_segment(offset);
page<slot>(pointer, pointer);
return;
}
@@ -621,7 +685,6 @@ private:
}
// MARK: - Memory Timing
// The wait mode affects all memory accesses _outside of the video area_.
enum class WaitMode {
None,
@@ -631,6 +694,7 @@ private:
bool is_video_[4]{};
// MARK: - ScanProducer
void set_scan_target(Outputs::Display::ScanTarget *const scan_target) override {
nick_.last_valid()->set_scan_target(scan_target);
}
@@ -706,11 +770,14 @@ private:
}
}
if(!media.file_bundles.empty()) {
host_fs_.set_file_bundle(media.file_bundles.front());
}
return true;
}
// MARK: - Interrupts
uint8_t interrupt_mask_ = 0x00, interrupt_state_ = 0x00;
void set_interrupts(const uint8_t mask, const HalfCycles offset = HalfCycles(0)) {
interrupt_state_ |= uint8_t(mask);
@@ -742,9 +809,69 @@ private:
}
// MARK: - EXDos card.
EXDos exdos_;
// MARK: - Host FS.
HostFSHandler host_fs_;
std::unordered_set<uint16_t> host_fs_traps_;
bool test_host_fs_traps_ = false;
uint8_t hostfs_read(const uint16_t address) override {
if(read_pointers_[address >> 14]) {
return read_pointers_[address >> 14][address];
} else {
return 0xff;
}
}
uint8_t &user_ram(const uint16_t address) {
// "User" accesses go to to wherever the user last had paged;
// per 5.4 System Segment Usage those pages are stored in memory from
// 0xbffc, so grab from there.
const auto page_id = address >> 14;
const uint8_t page = read_pointers_[0xbffc >> 14] ? read_pointers_[0xbffc >> 14][0xbffc + page_id] : 0xff;
const auto offset = address & 0x3fff;
return ram_segment(page)[offset];
}
uint8_t hostfs_user_read(const uint16_t address) override {
return user_ram(address);
}
void hostfs_user_write(const uint16_t address, const uint8_t value) override {
user_ram(address) = value;
}
void find_host_fs_hooks() {
static constexpr uint8_t syscall[] = {
0xed, 0xfe, 0xfe
};
auto begin = host_fs_rom_.begin();
while(true) {
begin = std::search(
begin, host_fs_rom_.end(),
std::begin(syscall), std::end(syscall)
);
if(begin == host_fs_rom_.end()) {
break;
}
const auto offset = begin - host_fs_rom_.begin() + 0xc000; // ROM will be paged in slot 3, i.e. at $c000.
host_fs_traps_.insert(uint16_t(offset));
// Move function code up to where this trap was, and NOP out the tail.
begin[0] = begin[3];
begin[1] = begin[2] = begin[3] = 0x00;
begin += 4;
}
}
// MARK: - Activity Source
void set_activity_observer([[maybe_unused]] Activity::Observer *const observer) final {
if constexpr (has_disk_controller) {
exdos_.set_activity_observer(observer);
@@ -752,6 +879,7 @@ private:
}
// MARK: - Configuration options.
std::unique_ptr<Reflection::Struct> get_options() const final {
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
options->output = get_video_signal_configurable();

View File

@@ -0,0 +1 @@
pyz80.py --obj=hostfs.rom hostfs.z80s && xxd -i hostfs.rom

View File

@@ -0,0 +1,215 @@
;
; Designed for assembly with pyz80, https://github.com/simonowen/pyz80/
; E.g. pyz80 --obj=hostfs.rom hostfs.z80s
;
;
; Sources:
;
; http://ep.homeserver.hu/Dokumentacio/Konyvek/EXOS_2.1_technikal_information/exos/kernel/Ch9.html
; on signature, device chain pointer and ROM entry point
;
; http://ep.homeserver.hu/Dokumentacio/Konyvek/EXOS_2.1_technikal_information/exos/kernel/Ch6.html
; on the device chain
;
;
; This code adapts the same mechanism for a host call as that used by EP128Emu's FILE IO ROM.
; My original thinking was that one could be substituted for the other to permit comparative testing.
; EP128 has a couple of emulator-specific call codes that I don't implement though, and otherwise
; doesn't seem to work in this emulator. And likely the converse holds.
;
hostfscall: macro
db 0xed, 0xfe, 0xfe
db \0
endm
exoscall: macro
rst 0x30
db \0
endm
org 0xc000
dm "EXOS_ROM" ; Standard ROM signature.
; Pointer to the included device chain, which should be valid when this
; ROM is paged at $4000, though when executed from it'll be at $c000.
dw 0x4000 + (device_chain & 0x3fff)
; ROM entry point; handle nothing.
ret
dw 0 ; XX_NEXT_LOW/HI: Pointer to start of next device. There is no next device.
dw 0xfffe ; XX_RAM_LOW/HI: [(Amount of host RAM used) + 2] negatived.
device_chain_type:
db 0 ; DD_TYPE: Type, which must be 0.
db 0 ; DD_IRQFLAG: No interrupts required.
db 0 ; DD_FLAGS: Not a video device.
dw 0x4000 + (dispatch & 0x3fff)
db 0 ; DD_TAB_LOW/HI/SEG:
db 0 ; DD_UNIT_COUNT: ?
device_name:
db 4
dm "FILE" ; DD_NAME
device_chain:
dw device_chain - device_chain_type
dispatch:
@dispatch: EQU FOR 14
dw call{@dispatch}
NEXT @dispatch
;
; Interrupt.
;
; The device chain indicates that this ROM doesn't receive interrupts. So no need to escalate.
;
call0:
ret
;
; Open channel.
;
; EXOS requires the programmer manually to call its function 27 to allocate a channel buffer if
; it otherwise expects to succeed. So some handling is most easily done within the client machine.
;
call1:
ld b, a ; Backup the channel number
hostfscall 1
call allocate_exos_buffer
ret z ; Exit on success.
; Otherwise, close the file and return the EXOS error.
ld c, a
ld a, b
hostfscall 3
ld a, c
ret
;
; Create channel.
;
call2:
ld b, a
hostfscall 2
call allocate_exos_buffer
ret z ; Exit on success.
; Otherwise, erase the newly-created file and return the EXOS error.
ld c, a
ld a, b
hostfscall 4
ld a, c
ret
;
; Close channel.
;
call3:
hostfscall 3
ret
;
; Destroy channel.
;
call4:
hostfscall 4
ret
;
; Read character.
;
call5:
hostfscall 5
ret
;
; Read block.
;
call6:
hostfscall 6
ret
;
; Write character.
;
call7:
hostfscall 7
ret
;
; Write block.
;
call8:
hostfscall 8
ret
;
; Read channel status.
;
call9:
hostfscall 9
ret
;
; Set channel status.
;
call10:
hostfscall 10
ret
;
; Special function.
;
call11:
hostfscall 11
ret
;
; Initialise.
;
call12:
hostfscall 12
;
; Set this as the default filing system.
; Disk dives do this, it's not unprecedented.
;
ld de, device_name
ld c, 1
exoscall 19
ret
;
; Buffer moved.
;
call13:
hostfscall 13
ret
;
; Attempts to allocate EXOS storage for a channel.
; Returns Z set for success, clear for failure.
;
allocate_exos_buffer:
; Exit immediately if that call already failed.
and a
ret nz
; Restore the channel number and otherwise configure to allocate a buffer.
push bc
ld a, b
ld bc, 0
ld de, 1
exoscall 27
; If there's no error from that, exit.
pop bc
and a
ret

View File

@@ -0,0 +1,262 @@
//
// HostFSHandler.cpp
// Clock Signal
//
// Created by Thomas Harte on 20/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#include "HostFSHandler.hpp"
#include "EXOSCodes.hpp"
using namespace Enterprise;
HostFSHandler::HostFSHandler(MemoryAccessor &accessor) : accessor_(accessor) {}
void HostFSHandler::perform(const uint8_t function, uint8_t &a, uint16_t &bc, uint16_t &de) {
const auto set_error = [&](const EXOS::Error error) {
a = uint8_t(error);
};
const auto set_b = [&](const uint8_t ch) {
bc = uint16_t((bc & 0xffff) | (ch << 8));
};
const auto set_c = [&](const uint8_t ch) {
bc = (bc & 0xff00) | (ch);
};
const auto b = [&]() -> uint8_t {
return bc >> 8;
};
const auto read_name = [&]() {
// Get name.
uint8_t length = accessor_.hostfs_read(de++);
std::string name;
while(length--) {
name.push_back(char(accessor_.hostfs_read(de++)));
}
// Use the key file if no name is specified.
if(name.empty()) {
if(const auto key_file = bundle_->key_file(); key_file.has_value()) {
name = *key_file;
}
}
return name;
};
//
// Functions that don't require an existing channel.
//
switch(function) {
default: break;
case uint8_t(EXOS::DeviceDescriptorFunction::Initialise):
channels_.clear();
set_error(EXOS::Error::NoError);
return;
case uint8_t(EXOS::DeviceDescriptorFunction::Interrupt):
case uint8_t(EXOS::DeviceDescriptorFunction::BufferMoved):
set_error(EXOS::Error::NoError);
return;
// Page 54.
// Emprically: C contains the unit number.
case uint8_t(EXOS::Function::OpenChannel): {
if(a == 255) {
set_error(EXOS::Error::ChannelIllegalOrDoesNotExist);
break;
}
const auto name = read_name();
try {
channels_.emplace(a, bundle_->open(name, Storage::FileMode::ReadWrite));
set_error(EXOS::Error::NoError);
} catch(Storage::FileHolder::Error) {
try {
channels_.emplace(a, bundle_->open(name, Storage::FileMode::Read));
set_error(EXOS::Error::NoError);
} catch(Storage::FileHolder::Error) {
// set_error(EXOS::Error::FileDoesNotExist);
set_error(EXOS::Error::ProtectionViolation);
}
}
}
return;
// Page 54.
case uint8_t(EXOS::Function::CreateChannel): {
if(a == 255) {
set_error(EXOS::Error::ChannelIllegalOrDoesNotExist);
break;
}
const auto name = read_name();
try {
channels_.emplace(a, bundle_->open(name, Storage::FileMode::Rewrite));
set_error(EXOS::Error::NoError);
} catch(Storage::FileHolder::Error) {
// set_error(EXOS::Error::FileAlreadyExists);
set_error(EXOS::Error::ProtectionViolation);
}
} return;
case uint8_t(EXOS::Function::SpecialFunction):
// Not supported;
set_error(EXOS::Error::InvalidSpecialFunctionCode);
return;
}
//
// Functions from here require a channel already open.
//
const auto channel = channels_.find(a);
if(channel == channels_.end()) {
set_error(EXOS::Error::ChannelIllegalOrDoesNotExist);
return;
}
auto &file = channel->second;
switch(function) {
default:
printf("UNIMPLEMENTED function %d with A:%02x BC:%04x DE:%04x\n", function, a, bc, de);
break;
// Page 54.
case uint8_t(EXOS::Function::CloseChannel):
set_error(EXOS::Error::NoError);
channels_.erase(channel);
break;
// Page 54.
case uint8_t(EXOS::Function::DestroyChannel): {
const auto name = file.name();
channels_.erase(channel);
if(bundle_->erase(name)) {
set_error(EXOS::Error::NoError);
} else {
set_error(EXOS::Error::ProtectionViolation);
}
} break;
// Page 55.
case uint8_t(EXOS::Function::ReadCharacter): {
const auto next = file.get();
if(file.eof()) {
set_error(EXOS::Error::EndOfFileMetInRead);
} else {
set_b(next);
set_error(EXOS::Error::NoError);
}
} break;
// Page 55.
case uint8_t(EXOS::Function::WriteCharacter): {
if(file.put(b())) {
set_error(EXOS::Error::NoError);
} else {
set_error(EXOS::Error::EndOfFileMetInRead);
}
} break;
// Page 55.
case uint8_t(EXOS::Function::ReadBlock): {
set_error(EXOS::Error::NoError);
while(bc) {
const auto next = file.get();
if(channel->second.eof()) {
set_error(EXOS::Error::EndOfFileMetInRead);
break;
}
accessor_.hostfs_user_write(de++, next);
--bc;
}
} break;
// Page 56.
case uint8_t(EXOS::Function::WriteBlock): {
set_error(EXOS::Error::NoError);
while(bc) {
const auto next = accessor_.hostfs_user_read(de);
if(!file.put(next)) {
set_error(EXOS::Error::EndOfFileMetInRead);
break;
}
++de;
--bc;
}
} break;
// Page 56.
case uint8_t(EXOS::Function::ReadChannelStatus):
a = file.eof() ? 0xff : 0x00;
break;
// Page 56.
case uint8_t(EXOS::Function::SetChannelStatus): {
if(bc & 4) {
// Protection byte is not supported.
set_error(EXOS::Error::FunctionNotSupported);
break;
}
if(bc & 1) { // User is requesting a seek.
auto pointer = de;
uint32_t file_pointer;
file_pointer = accessor_.hostfs_user_read(pointer++);
file_pointer |= uint32_t(accessor_.hostfs_user_read(pointer++) << 8);
file_pointer |= uint32_t(accessor_.hostfs_user_read(pointer++) << 16);
file_pointer |= uint32_t(accessor_.hostfs_user_read(pointer++) << 24);
if(!file.seek(file_pointer, Storage::Whence::SET)) {
set_error(EXOS::Error::EndOfFileMetInRead);
break;
}
}
// Fill in both position and length.
set_c(3);
const uint32_t file_pointer = uint32_t(file.tell());
const uint32_t file_length = uint32_t(file.stats().st_size);
auto pointer = de;
const auto write = [&](const uint32_t source) {
accessor_.hostfs_user_write(pointer++, uint8_t(source >> 0));
accessor_.hostfs_user_write(pointer++, uint8_t(source >> 8));
accessor_.hostfs_user_write(pointer++, uint8_t(source >> 16));
accessor_.hostfs_user_write(pointer++, uint8_t(source >> 24));
};
write(file_pointer);
write(file_length);
set_error(EXOS::Error::NoError);
} break;
}
}
void HostFSHandler::set_file_bundle(std::shared_ptr<Storage::FileBundle::FileBundle> bundle) {
bundle_ = bundle;
}
std::vector<uint8_t> HostFSHandler::rom() {
// Assembled and transcribed from hostfs.z80.
return std::vector<uint8_t>{
0x45, 0x58, 0x4f, 0x53, 0x5f, 0x52, 0x4f, 0x4d, 0x1b, 0x40, 0xc9, 0x00,
0x00, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x1d, 0x40, 0x00, 0x00, 0x04, 0x46,
0x49, 0x4c, 0x45, 0x0c, 0x00, 0x39, 0xc0, 0x3a, 0xc0, 0x4b, 0xc0, 0x5c,
0xc0, 0x61, 0xc0, 0x66, 0xc0, 0x6b, 0xc0, 0x70, 0xc0, 0x75, 0xc0, 0x7a,
0xc0, 0x7f, 0xc0, 0x84, 0xc0, 0x89, 0xc0, 0x95, 0xc0, 0xc9, 0x47, 0xed,
0xfe, 0xfe, 0x01, 0xcd, 0x9a, 0xc0, 0xc8, 0x4f, 0x78, 0xed, 0xfe, 0xfe,
0x03, 0x79, 0xc9, 0x47, 0xed, 0xfe, 0xfe, 0x02, 0xcd, 0x9a, 0xc0, 0xc8,
0x4f, 0x78, 0xed, 0xfe, 0xfe, 0x04, 0x79, 0xc9, 0xed, 0xfe, 0xfe, 0x03,
0xc9, 0xed, 0xfe, 0xfe, 0x04, 0xc9, 0xed, 0xfe, 0xfe, 0x05, 0xc9, 0xed,
0xfe, 0xfe, 0x06, 0xc9, 0xed, 0xfe, 0xfe, 0x07, 0xc9, 0xed, 0xfe, 0xfe,
0x08, 0xc9, 0xed, 0xfe, 0xfe, 0x09, 0xc9, 0xed, 0xfe, 0xfe, 0x0a, 0xc9,
0xed, 0xfe, 0xfe, 0x0b, 0xc9, 0xed, 0xfe, 0xfe, 0x0c, 0x11, 0x16, 0xc0,
0x0e, 0x01, 0xf7, 0x13, 0xc9, 0xed, 0xfe, 0xfe, 0x0d, 0xc9, 0xa7, 0xc0,
0xc5, 0x78, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0xf7, 0x1b, 0xc1, 0xa7,
0xc9
};
}

View File

@@ -0,0 +1,52 @@
//
// HostFSHandler.hpp
// Clock Signal
//
// Created by Thomas Harte on 20/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include "Storage/FileBundle/FileBundle.hpp"
#include <cstdint>
#include <memory>
#include <unordered_map>
#include <vector>
namespace Enterprise {
struct HostFSHandler {
struct MemoryAccessor {
// Accessors that read from however the Z80's 64kb is currently laid out.
virtual uint8_t hostfs_read(uint16_t) = 0;
// virtual void hostfs_write(uint16_t, uint8_t) = 0;
// Accessors that read from 'user' address space, i.e. the 64kb Z80 address space as currently
// mapped according to the user's preference.
virtual uint8_t hostfs_user_read(uint16_t) = 0;
virtual void hostfs_user_write(uint16_t, uint8_t) = 0;
};
HostFSHandler(MemoryAccessor &);
/// Perform the internally-defined @c function given other provided state.
/// These function calls mostly align with those in EXOSCodes.hpp
void perform(uint8_t function, uint8_t &a, uint16_t &bc, uint16_t &de);
/// Sets the bundle of files on which this handler should operate.
void set_file_bundle(std::shared_ptr<Storage::FileBundle::FileBundle> bundle);
/// @returns A suitable in-client filing system ROM.
std::vector<uint8_t> rom();
private:
MemoryAccessor &accessor_;
std::shared_ptr<Storage::FileBundle::FileBundle> bundle_;
using ChannelHandler = uint8_t;
std::unordered_map<ChannelHandler, Storage::FileHolder> channels_;
};
};

View File

@@ -1148,6 +1148,12 @@
4BCE0060227D39AB000CA200 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCE005E227D39AB000CA200 /* Video.cpp */; };
4BCE1DF125D4C3FA00AE7A2B /* Bus.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCE1DEF25D4C3FA00AE7A2B /* Bus.cpp */; };
4BCE1DF225D4C3FA00AE7A2B /* Bus.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCE1DEF25D4C3FA00AE7A2B /* Bus.cpp */; };
4BCF1ACF2ECE759000109999 /* FileBundle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1ACD2ECE759000109999 /* FileBundle.cpp */; };
4BCF1AD02ECE759000109999 /* FileBundle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1ACD2ECE759000109999 /* FileBundle.cpp */; };
4BCF1AD12ECE759000109999 /* FileBundle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1ACD2ECE759000109999 /* FileBundle.cpp */; };
4BCF1AD52ECF884100109999 /* HostFSHandler.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1AD42ECF884100109999 /* HostFSHandler.cpp */; };
4BCF1AD62ECF884100109999 /* HostFSHandler.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1AD42ECF884100109999 /* HostFSHandler.cpp */; };
4BCF1AD72ECF884100109999 /* HostFSHandler.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1AD42ECF884100109999 /* HostFSHandler.cpp */; };
4BCF1FA41DADC3DD0039D2E7 /* Oric.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1FA21DADC3DD0039D2E7 /* Oric.cpp */; };
4BD0FBC3233706A200148981 /* CSApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BD0FBC2233706A200148981 /* CSApplication.m */; };
4BD191F52191180E0042E144 /* ScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD191F22191180E0042E144 /* ScanTarget.cpp */; };
@@ -2425,6 +2431,11 @@
4BCE005F227D39AB000CA200 /* Video.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Video.hpp; sourceTree = "<group>"; };
4BCE1DEF25D4C3FA00AE7A2B /* Bus.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Bus.cpp; sourceTree = "<group>"; };
4BCE1DF025D4C3FA00AE7A2B /* Bus.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Bus.hpp; sourceTree = "<group>"; };
4BCF1ACC2ECE759000109999 /* FileBundle.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = FileBundle.hpp; sourceTree = "<group>"; };
4BCF1ACD2ECE759000109999 /* FileBundle.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = FileBundle.cpp; sourceTree = "<group>"; };
4BCF1AD22ECF743500109999 /* EXOSCodes.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = EXOSCodes.hpp; sourceTree = "<group>"; };
4BCF1AD32ECF884100109999 /* HostFSHandler.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = HostFSHandler.hpp; sourceTree = "<group>"; };
4BCF1AD42ECF884100109999 /* HostFSHandler.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = HostFSHandler.cpp; sourceTree = "<group>"; };
4BCF1FA21DADC3DD0039D2E7 /* Oric.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Oric.cpp; sourceTree = "<group>"; };
4BCF1FA31DADC3DD0039D2E7 /* Oric.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Oric.hpp; sourceTree = "<group>"; };
4BD060A51FE49D3C006E14BE /* Speaker.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Speaker.hpp; sourceTree = "<group>"; };
@@ -2700,11 +2711,14 @@
4BFEA2ED2682A7B900EBF94C /* Dave.cpp */,
4B051CA12676F52200CA44E8 /* Enterprise.cpp */,
4B051CB42680158600CA44E8 /* EXDos.cpp */,
4BCF1AD42ECF884100109999 /* HostFSHandler.cpp */,
4B051CAE267C1CA200CA44E8 /* Keyboard.cpp */,
4B051CAA26783E2000CA44E8 /* Nick.cpp */,
4BFEA2EE2682A7B900EBF94C /* Dave.hpp */,
4B051CA02676F52200CA44E8 /* Enterprise.hpp */,
4B051CB52680158600CA44E8 /* EXDos.hpp */,
4BCF1AD22ECF743500109999 /* EXOSCodes.hpp */,
4BCF1AD32ECF884100109999 /* HostFSHandler.hpp */,
4B051CAF267C1CA200CA44E8 /* Keyboard.hpp */,
4B051CAB26783E2000CA44E8 /* Nick.hpp */,
);
@@ -3587,6 +3601,7 @@
4BEE0A691D72496600532C7B /* Cartridge */,
4B8805F81DCFF6CD003085B1 /* Data */,
4BAB62AA1D3272D200DF5BA0 /* Disk */,
4BCF1ACE2ECE759000109999 /* FileBundle */,
4B6AAEA1230E3E1D0078E864 /* MassStorage */,
4B8DD3832634D37E00B3C866 /* State */,
4B69FB3A1C4D908A00B5F0AA /* Tape */,
@@ -5289,6 +5304,15 @@
path = ADB;
sourceTree = "<group>";
};
4BCF1ACE2ECE759000109999 /* FileBundle */ = {
isa = PBXGroup;
children = (
4BCF1ACC2ECE759000109999 /* FileBundle.hpp */,
4BCF1ACD2ECE759000109999 /* FileBundle.cpp */,
);
path = FileBundle;
sourceTree = "<group>";
};
4BCF1FA51DADC3E10039D2E7 /* Oric */ = {
isa = PBXGroup;
children = (
@@ -6197,6 +6221,7 @@
4B65086122F4CFE0009C1100 /* Keyboard.cpp in Sources */,
4BBB70A9202014E2002FE009 /* MultiProducer.cpp in Sources */,
4B2E86BF25D74F160024F1E9 /* Mouse.cpp in Sources */,
4BCF1AD12ECE759000109999 /* FileBundle.cpp in Sources */,
4B6ED2F1208E2F8A0047B343 /* WOZ.cpp in Sources */,
4B5D5C9825F56FC7001B4623 /* Spectrum.cpp in Sources */,
4B7C681727517A59001671EC /* Sprites.cpp in Sources */,
@@ -6339,6 +6364,7 @@
4B055AC41FAE9AE80060FFFF /* Keyboard.cpp in Sources */,
4B8DF506254E3C9D00F3433C /* ADB.cpp in Sources */,
4B055A941FAE85B50060FFFF /* CommodoreROM.cpp in Sources */,
4BCF1AD72ECF884100109999 /* HostFSHandler.cpp in Sources */,
4BBB70A5202011C2002FE009 /* MultiMediaTarget.cpp in Sources */,
4B8318BC22D3E588006DB630 /* DisplayMetrics.cpp in Sources */,
4BEDA40E25B2844B000C2DBD /* Decoder.cpp in Sources */,
@@ -6432,6 +6458,7 @@
4B1082C42C1F5E7D00B07C5D /* CSL.cpp in Sources */,
4B0ACC3023775819008902D0 /* TIASound.cpp in Sources */,
4B7136861F78724F008B8ED9 /* Encoder.cpp in Sources */,
4BCF1AD62ECF884100109999 /* HostFSHandler.cpp in Sources */,
4B0E04EA1FC9E5DA00F43484 /* CAS.cpp in Sources */,
4B7A90ED20410A85008514A2 /* StaticAnalyser.cpp in Sources */,
4B58601E1F806AB200AEE2E3 /* MFMSectorDump.cpp in Sources */,
@@ -6497,6 +6524,7 @@
4BEDA3BF25B25563000C2DBD /* Decoder.cpp in Sources */,
4B051C95266EF50200CA44E8 /* AppleIIController.swift in Sources */,
4B4DC82B1D2C27A4003C5BF8 /* SerialBus.cpp in Sources */,
4BCF1ACF2ECE759000109999 /* FileBundle.cpp in Sources */,
4BE8EB6625C750B50040BC40 /* DAT.cpp in Sources */,
4BBFFEE61F7B27F1005F3FEB /* TrackSerialiser.cpp in Sources */,
4B8855A52E84D51B00E251DD /* SAA5050.cpp in Sources */,
@@ -6857,8 +6885,10 @@
4B778F4023A5F1910000D260 /* z8530.cpp in Sources */,
4B778EFD23A5EB8E0000D260 /* AppleDSK.cpp in Sources */,
4B7752B728217EF40073E2C5 /* Chipset.cpp in Sources */,
4BCF1AD02ECE759000109999 /* FileBundle.cpp in Sources */,
4B06AAF72C64606E0034D014 /* DiskII.cpp in Sources */,
4B778EFB23A5EB7E0000D260 /* HFE.cpp in Sources */,
4BCF1AD52ECF884100109999 /* HostFSHandler.cpp in Sources */,
4BC751B21D157E61006C31D9 /* 6522Tests.swift in Sources */,
4B0DA67D282DCDF300C12F17 /* Instruction.cpp in Sources */,
4B06AAE12C645F8B0034D014 /* Video.cpp in Sources */,

View File

@@ -807,6 +807,29 @@
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>com</string>
<string>bas</string>
</array>
<key>CFBundleTypeIconFile</key>
<string></string>
<key>CFBundleTypeName</key>
<string>Enterprise Executable</string>
<key>CFBundleTypeOSTypes</key>
<array>
<string>????</string>
</array>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSTypeIsPackage</key>
<false/>
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>

View File

@@ -146,6 +146,7 @@ SOURCES += \
$$SRC/Storage/Disk/Encodings/MFM/*.cpp \
$$SRC/Storage/Disk/Parsers/*.cpp \
$$SRC/Storage/Disk/Track/*.cpp \
$$SRC/Storage/FileBundle/*.cpp \
$$SRC/Storage/MassStorage/*.cpp \
$$SRC/Storage/MassStorage/Encodings/*.cpp \
$$SRC/Storage/MassStorage/Formats/*.cpp \
@@ -303,6 +304,7 @@ HEADERS += \
$$SRC/Storage/Disk/Encodings/MFM/*.hpp \
$$SRC/Storage/Disk/Parsers/*.hpp \
$$SRC/Storage/Disk/Track/*.hpp \
$$SRC/Storage/FileBundle/*.hpp \
$$SRC/Storage/MassStorage/*.hpp \
$$SRC/Storage/MassStorage/Encodings/*.hpp \
$$SRC/Storage/MassStorage/Formats/*.hpp \

View File

@@ -134,6 +134,7 @@ SOURCES += glob.glob('../../Storage/Disk/Encodings/MFM/*.cpp')
SOURCES += glob.glob('../../Storage/Disk/Parsers/*.cpp')
SOURCES += glob.glob('../../Storage/Disk/Track/*.cpp')
SOURCES += glob.glob('../../Storage/Disk/Data/*.cpp')
SOURCES += glob.glob('../../Storage/FileBundle/*.cpp')
SOURCES += glob.glob('../../Storage/MassStorage/*.cpp')
SOURCES += glob.glob('../../Storage/MassStorage/Encodings/*.cpp')
SOURCES += glob.glob('../../Storage/MassStorage/Formats/*.cpp')

View File

@@ -0,0 +1,35 @@
//
// FileBundle.cpp
// Clock Signal
//
// Created by Thomas Harte on 19/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#include "FileBundle.hpp"
#include <cstdio>
using namespace Storage::FileBundle;
LocalFSFileBundle::LocalFSFileBundle(const std::string &to_contain) {
const auto last_separator = to_contain.find_last_of("/\\");
if(last_separator == std::string::npos) {
key_file_ = to_contain;
} else {
base_path_ = to_contain.substr(0, last_separator + 1);
key_file_ = to_contain.substr(last_separator + 1);
}
}
std::optional<std::string> LocalFSFileBundle::key_file() {
return key_file_;
}
Storage::FileHolder LocalFSFileBundle::open(const std::string &name, const Storage::FileMode mode) {
return Storage::FileHolder(base_path_ + name, mode);
}
bool LocalFSFileBundle::erase(const std::string &name) {
return !remove((base_path_ + name).c_str());
}

View File

@@ -0,0 +1,44 @@
//
// FileBundle.hpp
// Clock Signal
//
// Created by Thomas Harte on 19/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include "Storage/FileHolder.hpp"
#include <optional>
#include <string>
namespace Storage::FileBundle {
/*!
A File Bundle is a collection of individual files, abstracted from whatever media they might
be one.
Initial motivation is allowing some machines direct local filesystem access. An attempt has
been made to draft this in such a way as to allow it to do things like expose ZIP files as
bundles in the future.
*/
struct FileBundle {
virtual std::optional<std::string> key_file() = 0;
virtual FileHolder open(const std::string &, FileMode) = 0;
virtual bool erase(const std::string &) = 0;
};
struct LocalFSFileBundle: public FileBundle {
LocalFSFileBundle(const std::string &to_contain);
std::optional<std::string> key_file() override;
FileHolder open(const std::string &, FileMode) override;
bool erase(const std::string &) override;
private:
std::string key_file_;
std::string base_path_;
};
};

View File

@@ -13,6 +13,12 @@
using namespace Storage;
FileHolder::FileHolder(FileHolder &&rhs) {
file_ = rhs.file_;
rhs.file_ = nullptr;
// TODO: this leaves the RHS in an invalid state, which isn't appropriate for move semantics.
}
FileHolder::~FileHolder() {
if(file_) std::fclose(file_);
}
@@ -45,8 +51,8 @@ uint8_t FileHolder::get() {
return uint8_t(std::fgetc(file_));
}
void FileHolder::put(const uint8_t value) {
std::fputc(value, file_);
bool FileHolder::put(const uint8_t value) {
return std::fputc(value, file_) == value;
}
void FileHolder::putn(std::size_t repeats, const uint8_t value) {
@@ -71,9 +77,9 @@ std::size_t FileHolder::write(const uint8_t *buffer, const std::size_t size) {
return std::fwrite(buffer, 1, size, file_);
}
void FileHolder::seek(const long offset, const Whence whence) {
[[maybe_unused]] const auto result = std::fseek(file_, offset, int(whence));
assert(!result);
bool FileHolder::seek(const long offset, const Whence whence) {
const auto result = std::fseek(file_, offset, int(whence));
return !result;
}
long FileHolder::tell() const {

View File

@@ -56,9 +56,10 @@ public:
Rewrite opens the file for rewriting; none of the original content is preserved; whatever
the caller outputs will replace the existing file.
@throws ErrorCantOpen if the file cannot be opened.
@throws Error::CantOpen if the file cannot be opened.
*/
FileHolder(const std::string &file_name, FileMode ideal_mode = FileMode::ReadWrite);
FileHolder(FileHolder &&);
/*!
Writes @c value using successive @c puts, in little endian order.
@@ -116,8 +117,11 @@ public:
/*! Reads a single byte from @c file. */
uint8_t get();
/*! Writes a single byte from @c file. */
void put(uint8_t);
/*!
Writes a single byte from @c file.
@returns @c true on success; @c false on failure.
*/
bool put(uint8_t);
/*! Writes @c value a total of @c repeats times. */
void putn(std::size_t repeats, uint8_t value);
@@ -140,7 +144,7 @@ public:
std::size_t write(const uint8_t *, std::size_t);
/*! Moves @c bytes from the anchor indicated by @c whence: SEEK_SET, SEEK_CUR or SEEK_END. */
void seek(long offset, Whence);
bool seek(long offset, Whence);
/*! @returns The current cursor position within this file. */
long tell() const;

View File

@@ -130,6 +130,7 @@ set(CLK_SOURCES
Machines/Enterprise/Dave.cpp
Machines/Enterprise/EXDos.cpp
Machines/Enterprise/Enterprise.cpp
Machines/Enterprise/HostFSHandler.cpp
Machines/Enterprise/Keyboard.cpp
Machines/Enterprise/Nick.cpp
Machines/KeyboardMachine.cpp
@@ -224,6 +225,7 @@ set(CLK_SOURCES
Storage/Disk/Track/PCMTrack.cpp
Storage/Disk/Track/TrackSerialiser.cpp
Storage/Disk/Track/UnformattedTrack.cpp
Storage/FileBundle/FileBundle.cpp
Storage/FileHolder.cpp
Storage/MassStorage/Encodings/MacintoshVolume.cpp
Storage/MassStorage/Formats/DAT.cpp