From 321244520ca3e874512b8dff783dfc6414108aed Mon Sep 17 00:00:00 2001 From: wilbertpol Date: Mon, 20 May 2024 12:12:27 +0100 Subject: lib/formats/fs_cbmdos.cpp: Add partial write support. (#12389) --- src/lib/formats/fs_cbmdos.cpp | 387 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 363 insertions(+), 24 deletions(-) diff --git a/src/lib/formats/fs_cbmdos.cpp b/src/lib/formats/fs_cbmdos.cpp index cbf8ebed553..bf0da5511bc 100644 --- a/src/lib/formats/fs_cbmdos.cpp +++ b/src/lib/formats/fs_cbmdos.cpp @@ -1,5 +1,5 @@ // license:BSD-3-Clause -// copyright-holders:Nathan Woods +// copyright-holders:Nathan Woods, Wilbert Pol /*************************************************************************** fs_cbmdos.cpp @@ -8,6 +8,11 @@ http://fileformats.archiveteam.org/wiki/CBMFS +Current limitations: +- Writing is limited to the first 35 tracks. +- Determine or select the file type. Currently defaulting to PRG; 0 byte files + will be assigned file type DEL. + ***************************************************************************/ #include "fs_cbmdos.h" @@ -15,10 +20,12 @@ #include "fsblk.h" #include "corestr.h" +#include "multibyte.h" #include "strformat.h" #include #include +#include #include #include #include @@ -59,6 +66,8 @@ public: const void *data() const; const std::array &dirent_data() const; u8 size() const; + u8 track() const { return m_track; } + u8 sector() const { return m_sector; } private: const impl & m_fs; @@ -66,6 +75,8 @@ public: std::set> m_visited_set; u8 m_track; u8 m_sector; + u8 m_next_track; + u8 m_next_sector; }; impl(fsblk_t &blockdev); @@ -75,12 +86,52 @@ public: virtual std::pair metadata(const std::vector &path) override; virtual std::pair> directory_contents(const std::vector &path) override; virtual std::pair> file_read(const std::vector &path) override; + virtual err_t file_create(const std::vector &path, const meta_data &meta) override; + virtual err_t file_write(const std::vector &path, const std::vector &data) override; private: + static constexpr u32 BLOCK_SIZE = 256; + static constexpr u8 DIRECTORY_ENTRY_SIZE = 0x20; + static constexpr u8 FILE_TYPE_DEL = 0x80; + static constexpr u8 FILE_TYPE_SEQ = 0x81; + static constexpr u8 FILE_TYPE_PRG = 0x82; + static constexpr u8 FILE_TYPE_USR = 0x83; + static constexpr u8 FILE_TYPE_REL = 0x84; + static constexpr u8 TRACK_VARIANTS = 5; + static constexpr u8 MAX_SECTORS = 21; + static constexpr u8 MAX_TRACKS = 40; + static constexpr u8 DIRECTORY_TRACK = 18; + static constexpr u8 BAM_SECTOR = 0; + static constexpr u8 FIRST_DIRECTORY_SECTOR = 1; + static constexpr u8 SECTOR_DATA_BYTES = 254; + static constexpr u8 OFFSET_FILE_TYPE = 0x02; + static constexpr u8 OFFSET_FILE_FIRST_TRACK = 0x03; + static constexpr u8 OFFSET_FILE_FIRST_SECTOR = 0x04; + static constexpr u8 OFFSET_FILE_NAME = 0x05; + static constexpr u8 OFFSET_SECTOR_COUNT = 0x1e; + static constexpr u8 OFFSET_CHAIN_TRACK = 0x00; + static constexpr u8 OFFSET_CHAIN_SECTOR = 0x01; + static constexpr u8 CHAIN_END = 0x00; + + static const struct smap + { + u8 first_track; + u8 last_track; + int sector[MAX_SECTORS]; + } s_track_sector_map[TRACK_VARIANTS]; + static const u8 s_data_track_order[MAX_TRACKS - 1]; + u8 m_max_track; + fsblk_t::block_t read_sector(int track, int sector) const; std::optional dirent_from_path(const std::vector &path) const; - void iterate_directory_entries(const std::function &callback) const; + void iterate_directory_entries(const std::function &callback) const; + void iterate_all_directory_entries(const std::function &callback) const; meta_data metadata_from_dirent(const cbmdos_dirent &dirent) const; + bool is_valid_filename(const std::string &filename) const; + std::pair claim_track_sector(u8 track) const; + std::tuple claim_sector() const; + err_t free_sector(u8 track, u8 sector) const; + u8 determine_file_type(const std::vector &data) const; }; // methods @@ -147,7 +198,7 @@ bool fs::cbmdos_image::can_read() const bool fs::cbmdos_image::can_write() const { - return false; + return true; } @@ -225,8 +276,22 @@ std::string_view strtrimright_cbm(const char (&str)[N]) // impl ctor //------------------------------------------------- +const u8 impl::s_data_track_order[MAX_TRACKS - 1] = { + 17, 19, 16, 20, 15, 21, 14, 22, 13, 23, 12, 24, 11, 25, 10, 26, 9, 27, 8, 28, 7, 29, 6, 30, 5, 31, 4, 32, 3, 33, 2, 34, 1, 35, 36, 37, 38, 39, 40 +}; + +const impl::smap impl::s_track_sector_map[TRACK_VARIANTS] = { + { 1, 17, { 0, 11, 1, 12, 2, 13, 3, 14, 4, 15, 5, 16, 6, 17, 7, 18, 8, 19, 9, 20, 10 } }, + { 18, 18, { 0, 1, 4, 7, 10, 13, 16, 2, 5, 8, 11, 14, 17, 3, 6, 9, 12, 15, 18, -1, -1 } }, + { 19, 24, { 0, 10, 1, 11, 2, 12, 3, 13, 4, 14, 5, 15, 6, 16, 7, 17, 8, 18, 9, -1, -1 } }, + { 25, 30, { 0, 9, 1, 10, 2, 11, 3, 12, 4, 13, 5, 14, 6, 15, 7, 16, 8, 17, -1, -1, -1 } }, + { 31, 40, { 0, 9, 1, 10, 2, 11, 3, 12, 4, 13, 5, 14, 6, 15, 7, 16, 8, -1, -1, -1, -1 } } +}; + + impl::impl(fsblk_t &blockdev) - : filesystem_t(blockdev, 256) + : filesystem_t(blockdev, BLOCK_SIZE) + , m_max_track(35) { } @@ -237,7 +302,7 @@ impl::impl(fsblk_t &blockdev) meta_data impl::volume_metadata() { - auto bam_block = read_sector(18, 0); + auto bam_block = read_sector(DIRECTORY_TRACK, BAM_SECTOR); std::string_view disk_name = std::string_view((const char *) bam_block.rodata() + 0x90, 16); meta_data results; @@ -267,7 +332,7 @@ std::pair impl::metadata(const std::vector &path) std::pair> impl::directory_contents(const std::vector &path) { std::vector results; - auto callback = [this, &results](const cbmdos_dirent &ent) + auto callback = [this, &results](u8 track, u8 sector, u8 file_index, const cbmdos_dirent &ent) { results.emplace_back(dir_entry_type::file, metadata_from_dirent(ent)); return false; @@ -298,6 +363,262 @@ std::pair> impl::file_read(const std::vector } +err_t impl::file_create(const std::vector &path, const meta_data &meta) +{ + std::string filename = meta.get_string(meta_name::name, ""); + if (!is_valid_filename(filename)) + return ERR_INVALID; + + std::optional result; + u8 track = 0; + u8 sector = 0; + u8 file_index = 0; + auto callback = [&result, &track, §or, &file_index](u8 t, u8 s, u8 i, const cbmdos_dirent &dirent) + { + bool found = dirent.m_file_type == 0x00; + if (found) + { + result = dirent; + track = t; + sector = s; + file_index = i; + } + return found; + }; + iterate_all_directory_entries(callback); + + if (!result) + { + // Claim a next directory sector + auto [err, new_sector] = claim_track_sector(DIRECTORY_TRACK); + if (err != ERR_OK) + return err; + auto new_block = read_sector(DIRECTORY_TRACK, new_sector); + for (int i = 2; i < BLOCK_SIZE; i++) + new_block.w8(i, 0); + // Find last directory sector + u8 last_sector = 0; + block_iterator iter(*this, DIRECTORY_TRACK, FIRST_DIRECTORY_SECTOR); + while (iter.next()) + last_sector = iter.sector(); + // Update chain on last directory sector + auto last_dir_block = read_sector(DIRECTORY_TRACK, last_sector); + last_dir_block.w8(OFFSET_CHAIN_TRACK, DIRECTORY_TRACK); + last_dir_block.w8(OFFSET_CHAIN_SECTOR, new_sector); + track = DIRECTORY_TRACK; + sector = new_sector; + } + + auto [err, file_track, file_sector] = claim_sector(); + if (err != ERR_OK) + return err; + + // Create the file + auto dirblk = read_sector(track, sector); + u32 offset = file_index * DIRECTORY_ENTRY_SIZE; + for (int i = 0; i < DIRECTORY_ENTRY_SIZE; i++) + dirblk.w8(offset + i, (i >= 5 && i < 5 + 16) ? 0xa0 : 0x00); + dirblk.w8(offset + OFFSET_FILE_TYPE, FILE_TYPE_PRG); + dirblk.w8(offset + OFFSET_FILE_FIRST_TRACK, file_track); + dirblk.w8(offset + OFFSET_FILE_FIRST_SECTOR, file_sector); + dirblk.wstr(offset + OFFSET_FILE_NAME, filename); + // TODO set first side sector block track (rel file) + // TODO set first side sector block sector (rel file) + // TODO set rel file record length + // sector count will be set while writing the data + + return ERR_OK; +} + + +err_t impl::file_write(const std::vector &path, const std::vector &data) +{ + if (path.size() != 1) + return ERR_NOT_FOUND; + std::string_view path_part = path[0]; + + std::optional result; + u8 dir_track = 0; + u8 dir_sector = 0; + u8 dir_file_index = 0; + auto callback = [&result, &dir_track, &dir_sector, &dir_file_index, path_part](u8 track, u8 sector, u8 file_index, const cbmdos_dirent &dirent) + { + bool found = strtrimright_cbm(dirent.m_file_name) == path_part; + if (found) + { + dir_track = track; + dir_sector = sector; + dir_file_index = file_index; + result = dirent; + } + return found; + }; + iterate_directory_entries(callback); + + if (!result) + return ERR_NOT_FOUND; + + u8 data_track = result->m_file_first_track; + u8 data_sector = result->m_file_first_sector; + + const size_t data_length = data.size(); + size_t offset = 0; + u32 sector_count = 0; + while (offset < data_length) + { + auto datablk = read_sector(data_track, data_sector); + u8 bytes = (data_length - offset) > SECTOR_DATA_BYTES ? SECTOR_DATA_BYTES : data_length - offset; + memcpy(datablk.data() + 2, data.data() + offset, bytes); + offset += SECTOR_DATA_BYTES; + sector_count++; + if (datablk.r8(OFFSET_CHAIN_TRACK) == CHAIN_END) + { + if (offset < data_length) + { + auto [err, next_track, next_sector] = claim_sector(); + if (err != ERR_OK) + return err; + datablk.w8(OFFSET_CHAIN_TRACK, next_track); + datablk.w8(OFFSET_CHAIN_SECTOR, next_sector); + data_track = next_track; + data_sector = next_sector; + } + } + else + { + if (offset < data_length) + { + data_track = datablk.r8(OFFSET_CHAIN_TRACK); + data_sector = datablk.r8(OFFSET_CHAIN_SECTOR); + } + else + { + // Free the rest of the chain + u8 track_to_free = datablk.r8(OFFSET_CHAIN_TRACK); + u8 sector_to_free = datablk.r8(OFFSET_CHAIN_SECTOR); + + while (track_to_free != CHAIN_END) + { + err_t err = free_sector(track_to_free, sector_to_free); + if (err != ERR_OK) + return err; + datablk = read_sector(track_to_free, sector_to_free); + track_to_free = datablk.r8(OFFSET_CHAIN_TRACK); + sector_to_free = datablk.r8(OFFSET_CHAIN_SECTOR); + } + } + } + if (offset >= data_length) + { + datablk.w8(OFFSET_CHAIN_TRACK, CHAIN_END); + datablk.w8(OFFSET_CHAIN_SECTOR, bytes + 1); + } + } + + // Write sector count and file type to directory entry + auto dirblk = read_sector(dir_track, dir_sector); + u8 file_type = determine_file_type(data); + if (file_type == FILE_TYPE_DEL) + { + // Free sector, update first file sector to 00 + u8 file_track = dirblk.r8(DIRECTORY_ENTRY_SIZE * dir_file_index + OFFSET_FILE_FIRST_TRACK); + u8 file_sector = dirblk.r8(DIRECTORY_ENTRY_SIZE * dir_file_index + OFFSET_FILE_FIRST_SECTOR); + err_t err = free_sector(file_track, file_sector); + if (err != ERR_OK) + return err; + dirblk.w8(DIRECTORY_ENTRY_SIZE * dir_file_index + OFFSET_FILE_FIRST_TRACK, 0); + dirblk.w8(DIRECTORY_ENTRY_SIZE * dir_file_index + OFFSET_FILE_FIRST_SECTOR, 0); + sector_count = 0; + } + dirblk.w8(DIRECTORY_ENTRY_SIZE * dir_file_index + OFFSET_FILE_TYPE, file_type); + dirblk.w16l(DIRECTORY_ENTRY_SIZE * dir_file_index + OFFSET_SECTOR_COUNT, sector_count); + + return ERR_OK; +} + + +u8 impl::determine_file_type(const std::vector &data) const +{ + // TODO: Determine/set the actual file type, defaulting to PRG for now. + const size_t data_length = data.size(); + if (data_length == 0) + return FILE_TYPE_DEL; + return FILE_TYPE_PRG; +} + + +bool impl::is_valid_filename(const std::string &filename) const +{ + // We only support a subset of the character set supported by Commodore for filenames. + std::regex filename_regex("[A-Z0-9!\"#\\$%&'\\(\\)*+,-./:;<=>?]{1,16}"); + return std::regex_match(filename, filename_regex); +} + + +std::pair impl::claim_track_sector(u8 track) const +{ + if (track == 0 || track > m_max_track) + return std::make_pair(ERR_INVALID, 0); + + u8 map_index; + for (map_index = 0; map_index < TRACK_VARIANTS && !(s_track_sector_map[map_index].first_track <= track && track <= s_track_sector_map[map_index].last_track) ; map_index++); + if (map_index >= TRACK_VARIANTS) + return std::make_pair(ERR_INVALID, 0); + + auto bamblk = read_sector(DIRECTORY_TRACK, BAM_SECTOR); + u8 free_count = bamblk.r8(4 * track); + u32 free_bitmap = bamblk.r24l(4 * track + 1); + + for (int s = 0; s < MAX_SECTORS; s++) + { + int sector = s_track_sector_map[map_index].sector[s]; + if (sector >= 0 && free_count > 0 && util::BIT(free_bitmap, sector)) + { + free_bitmap &= ~(1 << sector); + free_count--; + bamblk.w8(4 * track, free_count); + bamblk.w24l(4 * track + 1, free_bitmap); + // Write chain end marker in new sector + auto claimedlk = read_sector(track, sector); + claimedlk.w8(OFFSET_CHAIN_TRACK, CHAIN_END); + claimedlk.w8(OFFSET_CHAIN_SECTOR, 0xff); + return std::make_pair(ERR_OK, sector); + } + } + return std::make_pair(ERR_NO_SPACE, 0); +} + + +std::tuple impl::claim_sector() const +{ + for (int track = 0; track < m_max_track - 1; track++) + { + auto [err, sector] = claim_track_sector(s_data_track_order[track]); + if (err == ERR_OK) + return std::make_tuple(ERR_OK, s_data_track_order[track], sector); + if (err != ERR_NO_SPACE) + return std::make_tuple(err, 0, 0); + } + return std::make_tuple(ERR_NO_SPACE, 0, 0); +} + + +err_t impl::free_sector(u8 track, u8 sector) const +{ + if (track == 0 || track > m_max_track) + return ERR_INVALID; + + auto bamblk = read_sector(DIRECTORY_TRACK, BAM_SECTOR); + u8 free_count = bamblk.r8(4 * track); + u32 free_bitmap = bamblk.r24l(4 * track + 1); + free_bitmap |= (1 << sector); + free_count++; + bamblk.w8(4 * track, free_count); + bamblk.w24l(4 * track + 1, free_bitmap); + return ERR_OK; +} + + //------------------------------------------------- // impl::read_sector //------------------------------------------------- @@ -326,7 +647,7 @@ std::optional impl::dirent_from_path(const std::vector result; - auto callback = [&result, path_part](const cbmdos_dirent &dirent) + auto callback = [&result, path_part](u8 track, u8 sector, u8 file_index, const cbmdos_dirent &dirent) { bool found = strtrimright_cbm(dirent.m_file_name) == path_part; if (found) @@ -342,22 +663,36 @@ std::optional impl::dirent_from_path(const std::vector &callback) const +void impl::iterate_directory_entries(const std::function &callback) const { - block_iterator iter(*this, 18, 1); + block_iterator iter(*this, DIRECTORY_TRACK, FIRST_DIRECTORY_SECTOR); while (iter.next()) { - for (const cbmdos_dirent &ent : iter.dirent_data()) - { - if (ent.m_file_type != 0x00) + auto entries = iter.dirent_data(); + + for (int file_index = 0; file_index < SECTOR_DIRECTORY_COUNT; file_index++) { + if (entries[file_index].m_file_type != 0x00) { - if (callback(ent)) + if (callback(iter.track(), iter.sector(), file_index, entries[file_index])) return; } } } } +void impl::iterate_all_directory_entries(const std::function &callback) const +{ + block_iterator iter(*this, DIRECTORY_TRACK, FIRST_DIRECTORY_SECTOR); + while (iter.next()) + { + auto entries = iter.dirent_data(); + + for (int file_index = 0; file_index < SECTOR_DIRECTORY_COUNT; file_index++) { + if (callback(iter.track(), iter.sector(), file_index, entries[file_index])) + return; + } + } +} //------------------------------------------------- // impl::metadata_from_dirent @@ -368,19 +703,19 @@ meta_data impl::metadata_from_dirent(const cbmdos_dirent &dirent) const std::string file_type; switch (dirent.m_file_type) { - case 0x80: + case FILE_TYPE_DEL: file_type = "DEL"; break; - case 0x81: + case FILE_TYPE_SEQ: file_type = "SEQ"; break; - case 0x82: + case FILE_TYPE_PRG: file_type = "PRG"; break; - case 0x83: + case FILE_TYPE_USR: file_type = "USR"; break; - case 0x84: + case FILE_TYPE_REL: file_type = "REL"; break; default: @@ -411,6 +746,8 @@ impl::block_iterator::block_iterator(const impl &fs, u8 first_track, u8 first_se : m_fs(fs) , m_track(first_track) , m_sector(first_sector) + , m_next_track(first_track) + , m_next_sector(first_sector) { } @@ -422,19 +759,21 @@ impl::block_iterator::block_iterator(const impl &fs, u8 first_track, u8 first_se bool impl::block_iterator::next() { bool result; - if (m_track != 0x00) + m_track = m_next_track; + m_sector = m_next_sector; + if (m_track != CHAIN_END) { // check for the degenerate scenario where we have a cycle (this should not happen // on a non-corrupt disk) - auto visited_tuple = std::make_tuple(m_track, m_sector); + auto visited_tuple = std::make_tuple(m_next_track, m_next_sector); if (m_visited_set.find(visited_tuple) != m_visited_set.end()) return false; m_visited_set.insert(visited_tuple); // with that out of the way, proceed - m_block = m_fs.read_sector(m_track, m_sector); - m_track = m_block.r8(0); - m_sector = m_block.r8(1); + m_block = m_fs.read_sector(m_next_track, m_next_sector); + m_next_track = m_block.r8(OFFSET_CHAIN_TRACK); + m_next_sector = m_block.r8(OFFSET_CHAIN_SECTOR); result = true; } else @@ -472,7 +811,7 @@ const std::array &impl::block u8 impl::block_iterator::size() const { - return m_track != 0x00 ? 254 : m_sector - 1; + return m_track != CHAIN_END ? SECTOR_DATA_BYTES : m_sector - 1; } } // anonymous namespace -- cgit v1.2.3