diff options
Diffstat (limited to 'src/lib/formats/fs_coco_rsdos.cpp')
-rw-r--r-- | src/lib/formats/fs_coco_rsdos.cpp | 743 |
1 files changed, 743 insertions, 0 deletions
diff --git a/src/lib/formats/fs_coco_rsdos.cpp b/src/lib/formats/fs_coco_rsdos.cpp new file mode 100644 index 00000000000..0ce88cf01df --- /dev/null +++ b/src/lib/formats/fs_coco_rsdos.cpp @@ -0,0 +1,743 @@ +// license:BSD-3-Clause +// copyright-holders:Nathan Woods, Wilbert Pol +/*************************************************************************** + + fs_coco_rsdos.cpp + + Management of CoCo "RS-DOS" floppy images + +Limitation: +- The determination of the file type is very limited. + +***************************************************************************/ + +#include "fs_coco_rsdos.h" +#include "coco_rawdsk.h" +#include "fsblk.h" + +#include "util/strformat.h" + +#include <bitset> +#include <optional> +#include <regex> +#include <string_view> + +using namespace fs; + +namespace fs { const coco_rsdos_image COCO_RSDOS; } + +namespace { + +class coco_rsdos_impl : public filesystem_t +{ +public: + coco_rsdos_impl(fsblk_t &blockdev); + virtual ~coco_rsdos_impl() = default; + + static constexpr int SECTOR_DIRECTORY_ENTRY_COUNT = 8; + + struct rsdos_dirent + { + char m_filename[11]; + u8 m_filetype; + u8 m_asciiflag; + u8 m_first_granule; + u8 m_last_sector_bytes_msb; + u8 m_last_sector_bytes_lsb; + }; + + struct rsdos_dirent_sector + { + struct + { + rsdos_dirent m_dirent; + u8 m_unused[16]; + } m_entries[SECTOR_DIRECTORY_ENTRY_COUNT]; + }; + + class granule_iterator + { + public: + granule_iterator(coco_rsdos_impl &fs, const rsdos_dirent &dirent); + bool next(u8 &granule, u16 &byte_count); + + private: + fsblk_t::block_t::ptr m_granule_map; + std::optional<u8> m_current_granule; + u8 m_maximum_granules; + u16 m_last_sector_bytes; + std::bitset<256> m_visited_granules; + }; + + virtual std::pair<std::error_condition, meta_data> metadata(const std::vector<std::string> &path) override; + virtual std::pair<std::error_condition, std::vector<dir_entry>> directory_contents(const std::vector<std::string> &path) override; + virtual std::pair<std::error_condition, std::vector<u8>> file_read(const std::vector<std::string> &path) override; + virtual std::error_condition format(const meta_data &meta) override; + virtual std::error_condition file_create(const std::vector<std::string> &path, const meta_data &meta) override; + virtual std::error_condition file_write(const std::vector<std::string> &path, const std::vector<u8> &data) override; + + static bool validate_filename(std::string_view name); + +private: + static constexpr u8 TRACK_GRANULE_COUNT = 2; + static constexpr u8 GRANULE_SECTOR_COUNT = 9; + static constexpr u8 TRACK_SECTOR_COUNT = TRACK_GRANULE_COUNT * GRANULE_SECTOR_COUNT; + static constexpr u8 DIRECTORY_TRACK = 17; + static constexpr u8 DIRECTORY_ENTRY_SIZE = 0x20; + static constexpr u8 FNAME_LENGTH = 11; + static constexpr u16 SECTOR_SIZE = 0x100; + static constexpr u8 OFFSET_FILE_TYPE = 0x0b; + static constexpr u8 OFFSET_ASCII_FLAG = 0x0c; + static constexpr u8 OFFSET_FIRST_GRANULE = 0x0d; + static constexpr u8 OFFSET_LAST_SECTOR_BYTES = 0x0e; + static constexpr u8 FILE_TYPE_BASIC = 0x00; + static constexpr u8 FILE_TYPE_BASIC_DATA = 0x01; + static constexpr u8 FILE_TYPE_MACHINE_CODE = 0x02; + static constexpr u8 FILE_TYPE_TEXT_EDITOR = 0x03; + static constexpr u8 FILE_LAST_GRANULE_INDICATOR = 0xc0; + + fsblk_t::block_t::ptr read_sector(int track, int sector) const; + fsblk_t::block_t::ptr read_granule_sector(u8 granule, u8 sector) const; + u8 maximum_granules() const; + std::optional<rsdos_dirent> dirent_from_path(const std::vector<std::string> &path); + template <typename T> + void iterate_directory_entries(T &&callback); + meta_data get_metadata_from_dirent(const rsdos_dirent &dirent); + static std::string get_filename_from_dirent(const rsdos_dirent &dirent); + std::pair<std::error_condition, std::string> build_direntry_filename(const std::string &filename); + std::pair<std::error_condition, u8> claim_granule(); + void write_granule_map(u8 granule, u8 map_data); + u8 read_granule_map(u8 granule) const; + bool is_ascii(const std::vector<u8> &data) const; + u8 determine_file_type(const std::vector<u8> &data) const; +}; + +} // anonymous namespace + + +//------------------------------------------------- +// name +//------------------------------------------------- + +const char *coco_rsdos_image::name() const +{ + return "coco_rsdos"; +} + + +//------------------------------------------------- +// description +//------------------------------------------------- + +const char *coco_rsdos_image::description() const +{ + return "CoCo RS-DOS"; +} + + +//------------------------------------------------- +// enumerate_f +//------------------------------------------------- + +void coco_rsdos_image::enumerate_f(floppy_enumerator &fe) const +{ + fe.add(FLOPPY_COCO_RAWDSK_FORMAT, floppy_image::FF_525, floppy_image::SSSD, 161280, "coco_rawdsk_rsdos_35", "CoCo Raw Disk RS-DOS single-sided 35 tracks"); + fe.add(FLOPPY_COCO_RAWDSK_FORMAT, floppy_image::FF_525, floppy_image::SSSD, 184320, "coco_rawdsk_rsdos_40", "CoCo Raw Disk RS-DOS single-sided 40 tracks"); +} + + +//------------------------------------------------- +// can_format +//------------------------------------------------- + +bool coco_rsdos_image::can_format() const +{ + return true; +} + + +//------------------------------------------------- +// can_read +//------------------------------------------------- + +bool coco_rsdos_image::can_read() const +{ + return true; +} + + +//------------------------------------------------- +// can_write +//------------------------------------------------- + +bool coco_rsdos_image::can_write() const +{ + return true; +} + + +//------------------------------------------------- +// has_rsrc +//------------------------------------------------- + +bool coco_rsdos_image::has_rsrc() const +{ + return false; +} + + +//------------------------------------------------- +// file_meta_description +//------------------------------------------------- + +std::vector<meta_description> coco_rsdos_image::file_meta_description() const +{ + std::vector<meta_description> results; + results.emplace_back(meta_description(meta_name::name, "", false, [](const meta_value &m) { return coco_rsdos_impl::validate_filename(m.as_string()); }, "File name, 8.3")); + results.emplace_back(meta_description(meta_name::file_type, 0, true, nullptr, "Type of the file")); + results.emplace_back(meta_description(meta_name::ascii_flag, "B", true, nullptr, "Ascii or binary flag")); + results.emplace_back(meta_description(meta_name::size_in_blocks, 0, true, nullptr, "Number of granules used by the file")); + results.emplace_back(meta_description(meta_name::length, 0, true, nullptr, "Size of the file in bytes")); + return results; +} + + +//------------------------------------------------- +// mount +//------------------------------------------------- + +std::unique_ptr<filesystem_t> coco_rsdos_image::mount(fsblk_t &blockdev) const +{ + return std::make_unique<coco_rsdos_impl>(blockdev); +} + + +//------------------------------------------------- +// validate_filename +//------------------------------------------------- + +bool coco_rsdos_impl::validate_filename(std::string_view name) +{ + auto pos = name.find('.'); + auto stem_length = pos != std::string::npos ? pos : name.size(); + auto ext_length = pos != std::string::npos ? name.size() - pos - 1 : 0; + return stem_length > 0 && stem_length <= 8 && ext_length <= 3; +} + + +//------------------------------------------------- +// coco_rsdos_impl ctor +//------------------------------------------------- + +coco_rsdos_impl::coco_rsdos_impl(fsblk_t &blockdev) + : filesystem_t(blockdev, SECTOR_SIZE) +{ +} + + +//------------------------------------------------- +// coco_rsdos_impl::metadata +//------------------------------------------------- + +std::pair<std::error_condition, meta_data> coco_rsdos_impl::metadata(const std::vector<std::string> &path) +{ + // attempt to find the file + const std::optional<rsdos_dirent> dirent = dirent_from_path(path); + if (!dirent) + return std::make_pair(error::not_found, meta_data()); + + return std::make_pair(std::error_condition(), get_metadata_from_dirent(*dirent)); +} + + +//------------------------------------------------- +// coco_rsdos_impl::directory_contents +//------------------------------------------------- + +std::pair<std::error_condition, std::vector<dir_entry>> coco_rsdos_impl::directory_contents(const std::vector<std::string> &path) +{ + std::vector<dir_entry> results; + auto const callback = [this, &results](u8 s, u8 i, const rsdos_dirent &dirent) + { + results.emplace_back(dir_entry_type::file, get_metadata_from_dirent(dirent)); + return false; + }; + iterate_directory_entries(callback); + return std::make_pair(std::error_condition(), std::move(results)); +} + + +//------------------------------------------------- +// coco_rsdos_impl::file_read +//------------------------------------------------- + +std::pair<std::error_condition, std::vector<u8>> coco_rsdos_impl::file_read(const std::vector<std::string> &path) +{ + // attempt to find the file + const std::optional<rsdos_dirent> dirent = dirent_from_path(path); + if (!dirent) + return std::make_pair(error::not_found, std::vector<u8>()); + + std::vector<u8> result; + u8 granule; + u16 byte_count; + granule_iterator iter(*this, *dirent); + while (iter.next(granule, byte_count)) + { + // resize the results + size_t current_size = result.size(); + result.resize(current_size + byte_count); + + // determine which track and sector this granule starts at + int track = granule / 2 + (granule >= 34 ? 1 : 0); + int sector = granule % 2 * 9 + 1; + + // and read all the sectors + while (byte_count > 0) + { + // read this sector + auto block = read_sector(track, sector); + u16 data_length = std::min(byte_count, u16(SECTOR_SIZE)); + + // and append it to the results + block->read(0, result.data() + current_size, data_length); + + // and advance + current_size += data_length; + byte_count -= data_length; + sector++; + } + } + return std::make_pair(std::error_condition(), std::move(result)); +} + + +//------------------------------------------------- +// coco_rsdos_impl::format +//------------------------------------------------- + +std::error_condition coco_rsdos_impl::format(const meta_data &meta) +{ + // formatting RS-DOS is easy - just fill everything with 0xFF + m_blockdev.fill_all(0xFF); + return std::error_condition(); +} + + +std::pair<std::error_condition, std::string> coco_rsdos_impl::build_direntry_filename(const std::string &filename) +{ + // The manual does not say anything about valid characters for a file name. + const std::regex filename_regex("([^.]{0,8})(\\.([^.]{0,3}))?"); + std::smatch smatch; + if (!std::regex_match(filename, smatch, filename_regex)) + return std::make_pair(error::invalid_name, std::string()); + if (smatch.size() != 4) + return std::make_pair(error::invalid_name, std::string()); + + std::string fname; + fname.resize(FNAME_LENGTH, ' '); + + for (int i = 0; i < 8 && i < smatch.str(1).size(); i++) + fname[i] = smatch.str(1)[i]; + + for (int j = 0; j < 3 && j < smatch.str(3).size(); j++) + fname[8 + j] = smatch.str(3)[j]; + + return std::make_pair(std::error_condition(), std::move(fname)); +} + + +std::error_condition coco_rsdos_impl::file_create(const std::vector<std::string> &path, const meta_data &meta) +{ + if (!path.empty()) + return error::unsupported; + + const std::string filename = meta.get_string(meta_name::name, ""); + auto [err, fname] = build_direntry_filename(filename); + if (err) + return err; + + bool found_entry = false; + u8 dir_sector = 0; + u8 file_index = 0; + for (dir_sector = 3; !found_entry && dir_sector <= TRACK_SECTOR_COUNT; dir_sector++) + { + auto dir_block = read_sector(DIRECTORY_TRACK, dir_sector); + + for (file_index = 0; !found_entry && file_index < SECTOR_DIRECTORY_ENTRY_COUNT; file_index++) + { + const u8 first_byte = dir_block->r8(file_index * DIRECTORY_ENTRY_SIZE); + // 0xff marks the end of the directory, 0x00 marks a deleted file + found_entry = (first_byte == 0xff || first_byte == 0x00); + if (found_entry) + { + for (int i = 0; i < DIRECTORY_ENTRY_SIZE; i++) + dir_block->w8(file_index * DIRECTORY_ENTRY_SIZE + i, 0); + + auto [cerr, granule] = claim_granule(); + if (cerr != std::error_condition()) + return cerr; + + dir_block->wstr(file_index * DIRECTORY_ENTRY_SIZE + 0, fname); + dir_block->w8(file_index * DIRECTORY_ENTRY_SIZE + OFFSET_FIRST_GRANULE, granule); + // The file type, ASCII flag, and number of bytes in last sector of file will be set during writing. + } + } + } + if (!found_entry) + return error::no_space; + + return std::error_condition(); +} + + +std::error_condition coco_rsdos_impl::file_write(const std::vector<std::string> &path, const std::vector<u8> &data) +{ + if (path.size() != 1) + return error::not_found; + const std::string &target = path[0]; + + std::optional<rsdos_dirent> result; + u8 dir_sector = 0; + u8 dir_file_index = 0; + auto const callback = [&result, &target, &dir_sector, &dir_file_index](u8 s, u8 i, const rsdos_dirent &dirent) + { + const bool found = get_filename_from_dirent(dirent) == target; + if (found) + { + result = dirent; + dir_sector = s; + dir_file_index = i; + } + return found; + }; + iterate_directory_entries(callback); + + if (!result) + return error::not_found; + + const size_t data_length = data.size(); + const u8 max_granule = maximum_granules(); + u8 granule = result->m_first_granule; + u8 granule_sector = 1; + size_t offset = 0; + u16 bytes_in_last_sector = 0; + u8 linked_granule = read_granule_map(granule); + + while (offset < data_length) + { + auto data_block = read_granule_sector(granule, granule_sector); + bytes_in_last_sector = (data_length - offset) > SECTOR_SIZE ? SECTOR_SIZE : data_length - offset; + data_block->write(0, data.data() + offset, bytes_in_last_sector); + offset += SECTOR_SIZE; + write_granule_map(granule, FILE_LAST_GRANULE_INDICATOR + granule_sector); + if (offset < data_length) + { + granule_sector++; + if (granule_sector > GRANULE_SECTOR_COUNT) + { + // Re-use linked granule or claim a new granule + if (linked_granule < max_granule) + { + granule = linked_granule; + linked_granule = read_granule_map(linked_granule); + } + else + { + auto [err, next_granule] = claim_granule(); + if (err) + return err; + write_granule_map(granule, next_granule); + granule = next_granule; + } + granule_sector = 1; + } + } + } + + // Free unused space + while (linked_granule < max_granule) + { + const u8 granule_to_free = linked_granule; + linked_granule = read_granule_map(granule_to_free); + write_granule_map(granule_to_free, 0xff); + } + + // Update directory entry + auto dir_block = read_sector(DIRECTORY_TRACK, dir_sector); + dir_block->w8(dir_file_index * DIRECTORY_ENTRY_SIZE + OFFSET_FILE_TYPE, determine_file_type(data)); + dir_block->w8(dir_file_index * DIRECTORY_ENTRY_SIZE + OFFSET_ASCII_FLAG, is_ascii(data) ? 0xff : 0x00); + dir_block->w16b(dir_file_index * DIRECTORY_ENTRY_SIZE + OFFSET_LAST_SECTOR_BYTES, bytes_in_last_sector); + + return std::error_condition(); +} + + +bool coco_rsdos_impl::is_ascii(const std::vector<u8> &data) const +{ + const size_t data_length = data.size(); + + if (data_length == 0) + return false; + + for (int i = 0; i < data_length; i++) + { + if (!(data[i] == 0x0d || data[i] == 0x0a || (data[i] >= 0x20 && data[i] < 0x60))) + return false; + } + + return true; +} + + +u8 coco_rsdos_impl::determine_file_type(const std::vector<u8> &data) const +{ + if (is_ascii(data)) + { + // TODO: Distinguish between BASIC code and text editor data + return FILE_TYPE_BASIC; + } + + const size_t data_length = data.size(); + // Binary BASIC code seems to begin with ff <16bit size> 26 + if (data_length > 4 && data[0] == 0xff && data[3] == 0x26 && ((data[1] << 8) | data[2]) == data_length - 3) + return FILE_TYPE_BASIC; + + // TODO: Distinguish between Machine Code and Basic Data + + return FILE_TYPE_MACHINE_CODE; +} + + +std::pair<std::error_condition, u8> coco_rsdos_impl::claim_granule() +{ + // Granules are likely not assigned in this order on hardware. + auto granule_block = read_sector(DIRECTORY_TRACK, 2); + for (int g = 0; g < maximum_granules(); g++) + { + if (granule_block->r8(g) == 0xff) + { + granule_block->w8(g, FILE_LAST_GRANULE_INDICATOR); + return std::make_pair(std::error_condition(), g); + } + } + return std::make_pair(error::no_space, 0); +} + + +void coco_rsdos_impl::write_granule_map(u8 granule, u8 map_data) +{ + auto granule_block = read_sector(DIRECTORY_TRACK, 2); + granule_block->w8(granule, map_data); +} + + +u8 coco_rsdos_impl::read_granule_map(u8 granule) const +{ + auto granule_block = read_sector(DIRECTORY_TRACK, 2); + return granule_block->r8(granule); +} + + +//------------------------------------------------- +// coco_rsdos_impl::read_sector +//------------------------------------------------- + +fsblk_t::block_t::ptr coco_rsdos_impl::read_sector(int track, int sector) const +{ + // the CoCo RS-DOS world thinks in terms of tracks/sectors, but we have a block device + // abstraction + return m_blockdev.get(track * TRACK_SECTOR_COUNT + sector - 1); +} + + +fsblk_t::block_t::ptr coco_rsdos_impl::read_granule_sector(u8 granule, u8 sector) const +{ + // Track 17 does not hold granules + if (granule < (DIRECTORY_TRACK * TRACK_GRANULE_COUNT)) + return m_blockdev.get(granule * GRANULE_SECTOR_COUNT + sector - 1); + else + return m_blockdev.get((granule + TRACK_GRANULE_COUNT) * GRANULE_SECTOR_COUNT + sector - 1); +} + + +//------------------------------------------------- +// coco_rsdos_impl::maximum_granules +//------------------------------------------------- + +u8 coco_rsdos_impl::maximum_granules() const +{ + u32 sector_count = m_blockdev.block_count(); + u32 granule_count = (sector_count / GRANULE_SECTOR_COUNT) - 2; + return granule_count <= 0xFF ? u8(granule_count) : 0xFF; +} + + +//------------------------------------------------- +// coco_rsdos_impl::dirent_from_path +//------------------------------------------------- + +std::optional<coco_rsdos_impl::rsdos_dirent> coco_rsdos_impl::dirent_from_path(const std::vector<std::string> &path) +{ + if (path.size() != 1) + return { }; + const std::string &target = path[0]; + + std::optional<rsdos_dirent> result; + auto const callback = [&result, &target](u8 s, u8 i, const rsdos_dirent &dirent) + { + bool found = get_filename_from_dirent(dirent) == target; + if (found) + result = dirent; + return found; + }; + iterate_directory_entries(callback); + return result; +} + + +//------------------------------------------------- +// coco_rsdos_impl::iterate_directory_entries +//------------------------------------------------- + +template <typename T> +void coco_rsdos_impl::iterate_directory_entries(T &&callback) +{ + bool done = false; + for (int dir_sector = 3; !done && dir_sector <= 18; dir_sector++) + { + // read this directory sector + auto dir_block = read_sector(DIRECTORY_TRACK, dir_sector); + const rsdos_dirent_sector §or = *reinterpret_cast<const rsdos_dirent_sector *>(dir_block->rodata()); + + // and loop through all entries + for (int file_index = 0; file_index < 8; file_index++) + { + // 0xFF marks the end of the directory + if (sector.m_entries[file_index].m_dirent.m_filename[0] == '\xFF') + { + done = true; + } + else + { + // 0x00 marks a deleted file + if (sector.m_entries[file_index].m_dirent.m_filename[0] != '\0') + done = callback(dir_sector, 0, sector.m_entries[file_index].m_dirent); + } + + if (done) + break; + } + } +} + + +//------------------------------------------------- +// coco_rsdos_impl::get_metadata_from_dirent +//------------------------------------------------- + +meta_data coco_rsdos_impl::get_metadata_from_dirent(const rsdos_dirent &dirent) +{ + u32 file_size = 0; + int granule_count = 0; + + // we need to iterate on the file to determine the size and granule/block count + u8 granule; + u16 byte_count; + granule_iterator iter(*this, dirent); + while (iter.next(granule, byte_count)) + { + granule_count++; + file_size += byte_count; + } + + // determine the file name + std::string name = get_filename_from_dirent(dirent); + + // turn the ASCII flag to a single character (this reflects what the user sees when doing a directory listing on a real CoCo) + char file_type_char = 'B' + dirent.m_asciiflag; + + // build the metadata and return it + meta_data result; + result.set(meta_name::name, std::move(name)); + result.set(meta_name::file_type, dirent.m_filetype); + result.set(meta_name::ascii_flag, std::string(1, file_type_char)); + result.set(meta_name::size_in_blocks, granule_count); + result.set(meta_name::length, file_size); + return result; +} + + +//------------------------------------------------- +// coco_rsdos_impl::get_filename_from_dirent +//------------------------------------------------- + +std::string coco_rsdos_impl::get_filename_from_dirent(const rsdos_dirent &dirent) +{ + std::string_view stem = trim_end_spaces(std::string_view(&dirent.m_filename[0], 8)); + std::string_view ext = trim_end_spaces(std::string_view(&dirent.m_filename[8], 3)); + return util::string_format("%s.%s", stem, ext); +} + + +//------------------------------------------------- +// coco_rsdos_impl::granule_iterator ctor +//------------------------------------------------- + +coco_rsdos_impl::granule_iterator::granule_iterator(coco_rsdos_impl &fs, const rsdos_dirent &dirent) + : m_granule_map(fs.read_sector(DIRECTORY_TRACK, 2)) + , m_current_granule(dirent.m_first_granule) + , m_maximum_granules(fs.maximum_granules()) + , m_last_sector_bytes((u16(dirent.m_last_sector_bytes_msb) << 8) | dirent.m_last_sector_bytes_lsb) +{ + // visit the first granule + m_visited_granules.set(dirent.m_first_granule); +} + + +//------------------------------------------------- +// coco_rsdos_impl::granule_iterator::next +//------------------------------------------------- + +bool coco_rsdos_impl::granule_iterator::next(u8 &granule, u16 &byte_count) +{ + bool success = false; + granule = ~0; + byte_count = 0; + + if (m_current_granule) + { + std::optional<u8> next_granule; + const u8 *granule_map_data = m_granule_map->rodata(); + if (granule_map_data[*m_current_granule] < m_maximum_granules) + { + // this entry points to the next granule + success = true; + granule = *m_current_granule; + byte_count = GRANULE_SECTOR_COUNT * SECTOR_SIZE; + next_granule = granule_map_data[*m_current_granule]; + + // check for cycles, which should only happen if the disk is corrupt (or not in RS-DOS format) + if (m_visited_granules[*next_granule]) + next_granule = std::nullopt; // this is corrupt! + else + m_visited_granules.set(*next_granule); + } + else if (granule_map_data[*m_current_granule] >= FILE_LAST_GRANULE_INDICATOR && granule_map_data[*m_current_granule] <= FILE_LAST_GRANULE_INDICATOR + GRANULE_SECTOR_COUNT) + { + // this is the last granule in the file + success = true; + granule = *m_current_granule; + u16 sector_count = std::max(granule_map_data[*m_current_granule], u8(0xC1)) - 0xC1; + byte_count = sector_count * SECTOR_SIZE + m_last_sector_bytes; + next_granule = std::nullopt; + } + else + { + // should not happen; but we'll treat this as an EOF + next_granule = std::nullopt; + } + m_current_granule = next_granule; + } + return success; +} |