// // MOOF.cpp // Clock Signal // // Created by Thomas Harte on 15/02/2026. // Copyright © 2026 Thomas Harte. All rights reserved. // #include "MOOF.hpp" #include "Storage/Disk/Track/PCMTrack.hpp" #include "Numeric/CRC.hpp" using namespace Storage::Disk; namespace { constexpr uint32_t chunk(const char *str) { return uint32_t(str[0] | (str[1] << 8) | (str[2] << 16) | (str[3] << 24)); } class MOOFFluxTrack: public Track { public: MOOFFluxTrack(std::vector &&data, const uint32_t bit_count) : data_(std::move(data)), bit_count_(bit_count) { seek_to(0.0f); } Event get_next_event() final { Event event; if(data_iterator_ == data_.end() || bits_passed_ == bit_count_) { data_iterator_ = data_.begin(); bits_passed_ = 0; return Event{ .type = Storage::Disk::Track::Event::IndexHole, .length = Storage::Time() }; } Event result; result.type = Storage::Disk::Track::Event::FluxTransition; result.length.clock_rate = 8'000'000; // i.e. 125ns result.length.length = 0; while(true) { const auto delta = *data_iterator_; result.length.length += delta; ++data_iterator_; if(delta != 0xff || data_iterator_ == data_.end()) { break; } } ++bits_passed_; return result; } float seek_to(const float time_since_index_hole) final { bits_passed_ = 0; data_iterator_ = data_.begin(); float time_observed = 0.0f; while(time_observed < time_since_index_hole) { auto prior = data_iterator_; const auto next_time = get_next_event().length.get(); if(time_observed + next_time > time_since_index_hole) { --bits_passed_; data_iterator_ = prior; break; } time_observed += next_time; } return time_observed; } Track *clone() const final { return new MOOFFluxTrack(data_, bit_count_); } private: MOOFFluxTrack(const std::vector &data, const uint32_t bit_count) : data_(data), bit_count_(bit_count) { seek_to(0.0f); } std::vector data_; uint32_t bit_count_ = 0; std::vector::iterator data_iterator_; uint32_t bits_passed_ = 0; }; } MOOF::MOOF(const std::string &file_name) : file_(file_name) { static constexpr char signature[] = { 'M', 'O', 'O', 'F', char(0xff), 0x0a, 0x0d, 0x0a }; if(!file_.check_signature(signature)) { throw Error::InvalidFormat; } // Test the file's CRC32. const auto crc = file_.get_le(); post_crc_contents_ = file_.read(size_t(file_.stats().st_size - 12)); const uint32_t computed_crc = CRC::CRC32::crc_of(post_crc_contents_); if(crc != computed_crc) { throw Error::InvalidFormat; } // Retreat to the first byte after the CRC and parse all chunks. file_.seek(12, Whence::SET); bool has_tmap = false, has_flux = false; std::fill(std::begin(track_map_), std::end(track_map_), 0xff); std::fill(std::begin(flux_map_), std::end(flux_map_), 0xff); while(true) { const auto chunk_id = file_.get_le(); const auto chunk_size = file_.get_le(); const auto end_of_chunk = file_.tell() + long(chunk_size); if(file_.eof()) break; switch(chunk_id) { case chunk("INFO"): info_.version = file_.get(); info_.disk_type = Info::DiskType(file_.get()); info_.is_write_protected = file_.get(); break; case chunk("TMAP"): file_.read(track_map_, 160); has_tmap = true; break; case chunk("FLUX"): file_.read(flux_map_, 160); has_flux = true; break; case chunk("TRKS"): tracks_offset_ = file_.tell(); break; default: break; } file_.seek(end_of_chunk, Whence::SET); } // Structural issues. if(tracks_offset_ == -1 || (!has_tmap && !has_flux)) { throw Error::InvalidFormat; } // Versioning. // // TODO: determine what it means to be a Twiggy disk, in encoding terms. const bool supports_disk_type = info_.disk_type == Info::DiskType::GCR400kb || info_.disk_type == Info::DiskType::GCR800kb || info_.disk_type == Info::DiskType::MFM; if(info_.version != 1 || !supports_disk_type) { throw Error::InvalidFormat; } } bool MOOF::is_read_only() const { // Writing not-yet implemented. return true; //info_.is_write_protected; } HeadPosition MOOF::maximum_head_position() const { return HeadPosition(80); } int MOOF::head_count() const { return info_.disk_type == Info::DiskType::GCR400kb ? 1 : 2; } std::unique_ptr MOOF::track_at_position(const Track::Address address) const { const int table_position = address.position.as_int() * 2 + address.head; if(table_position > 255) { return nullptr; } const auto location = [&](const uint8_t offset) -> TrackLocation { file_.seek(tracks_offset_ + 8 * long(offset), Whence::SET); TrackLocation location; location.starting_block = file_.get_le(); location.block_count = file_.get_le(); location.bit_count = file_.get_le(); return location; }; if(flux_map_[table_position] != 0xff) { return flux(location(flux_map_[table_position])); } else if(track_map_[table_position] != 0xff) { return track(location(track_map_[table_position])); } return nullptr; } std::unique_ptr MOOF::flux(const TrackLocation location) const { file_.seek(location.starting_block * 512, Whence::SET); auto track_contents = file_.read((location.bit_count + 7) / 8); return std::make_unique(std::move(track_contents), location.bit_count); } std::unique_ptr MOOF::track(const TrackLocation location) const { file_.seek(location.starting_block * 512, Whence::SET); const auto track_contents = file_.read((location.bit_count + 7) / 8); return std::make_unique(PCMSegment(location.bit_count, track_contents)); } bool MOOF::represents(const std::string &name) const { return name == file_.name(); } void MOOF::set_tracks([[maybe_unused]] const std::map> &tracks) { // for(const auto &[address, track]: tracks) { // TODO: // // (1) can this be a PCM track, or does it need to be a flux track? // (2) manipulate the many indirections of a MOOF file. // // To consider: would it be better to do MOOFs and WOZs with a higher-level // data structure? // } }