diff --git a/src/lib/formats/fs_coco_os9.cpp b/src/lib/formats/fs_coco_os9.cpp index c4fcd14ea16..89e41175de7 100644 --- a/src/lib/formats/fs_coco_os9.cpp +++ b/src/lib/formats/fs_coco_os9.cpp @@ -24,13 +24,104 @@ namespace fs { const coco_os9_image COCO_OS9; } namespace { +// ======================> coco_os9_impl + class coco_os9_impl : public filesystem_t { public: - coco_os9_impl(fsblk_t &blockdev); - virtual ~coco_os9_impl() = default; + // ======================> volume_header + class volume_header + { + public: + volume_header(fsblk_t::block_t &&block); + volume_header(const volume_header &) = delete; + volume_header(volume_header &&) = default; + + u32 total_sectors() const { return m_block.r24b(0); } + u8 track_size_in_sectors() const { return m_block.r8(3); } + u16 allocation_bitmap_bytes() const { return m_block.r16b(4); } + u16 cluster_size() const { return m_block.r16b(6); } + u32 root_dir_lsn() const { return m_block.r24b(8); } + u16 owner_id() const { return m_block.r16b(11); } + u16 disk_id() const { return m_block.r16b(14); } + u8 format_flags() const { return m_block.r8(16); } + u16 sectors_per_track() const { return m_block.r16b(17); } + u32 bootstrap_lsn() const { return m_block.r24b(21); } + u16 bootstrap_size() const { return m_block.r16b(24); } + util::arbitrary_datetime creation_date() const { return from_os9_date(m_block.r24b(26), m_block.r16b(29)); } + u16 sector_size() const { u16 result = m_block.r16b(104); return result != 0 ? result : 256; } + u8 sides() const { return (format_flags() & 0x01) ? 2 : 1; } + bool double_density() const { return (format_flags() & 0x02) != 0; } + bool double_track() const { return (format_flags() & 0x04) != 0; } + bool quad_track_density() const { return (format_flags() & 0x08) != 0; } + bool octal_track_density() const { return (format_flags() & 0x10) != 0; } + + std::string name() const; + + private: + fsblk_t::block_t m_block; + }; + + + // ======================> file_header + + class file_header + { + public: + file_header(fsblk_t::block_t &&block, std::string &&filename); + file_header(const file_header &) = delete; + file_header(file_header &&) = default; + + file_header &operator=(const file_header &) = delete; + file_header &operator=(file_header &&) = default; + + u8 attributes() const { return m_block.r8(0); } + u16 owner_id() const { return m_block.r16b(1); } + u8 link_count() const { return m_block.r8(8); } + u32 file_size() const { return m_block.r32b(9); } + util::arbitrary_datetime creation_date() const; + bool is_directory() const { return (attributes() & 0x80) != 0; } + bool is_non_sharable() const { return (attributes() & 0x40) != 0; } + bool is_public_execute() const { return (attributes() & 0x20) != 0; } + bool is_public_write() const { return (attributes() & 0x10) != 0; } + bool is_public_read() const { return (attributes() & 0x08) != 0; } + bool is_user_execute() const { return (attributes() & 0x04) != 0; } + bool is_user_write() const { return (attributes() & 0x02) != 0; } + bool is_user_read() const { return (attributes() & 0x01) != 0; } + + meta_data metadata() const; + int get_sector_map_entry_count() const; + void get_sector_map_entry(int entry_number, u32 &start_lsn, u16 &count) const; + + private: + fsblk_t::block_t m_block; + std::string m_filename; + }; + + // ctor/dtor + coco_os9_impl(fsblk_t &blockdev, volume_header &&header); + virtual ~coco_os9_impl() = default; + + virtual meta_data volume_metadata() override; + 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 format(const meta_data &meta) override; + + std::optional find(const std::vector &path, std::optional expected_entry_type) const; + void iterate_directory_entries(const file_header &header, const std::function callback) const; + std::vector read_file_data(const file_header &header) const; + + static std::string pick_os9_string(std::string_view raw_string); + static std::string to_os9_string(std::string_view s, size_t length); + static u32 pick_integer_be(const u8 *data, int length); + static util::arbitrary_datetime from_os9_date(u32 os9_date, u16 os9_time = 0); + static std::tuple to_os9_date(const util::arbitrary_datetime &datetime); static bool is_ignored_filename(std::string_view name); static bool validate_filename(std::string_view name); + +private: + volume_header m_volume_header; }; } @@ -166,18 +257,405 @@ std::vector coco_os9_image::directory_meta_description() const std::unique_ptr coco_os9_image::mount(fsblk_t &blockdev) const { - return std::make_unique(blockdev); + // read the header block + blockdev.set_block_size(256); + coco_os9_impl::volume_header header(blockdev.get(0)); + + // sanity checks + if (header.sectors_per_track() != header.track_size_in_sectors()) + return { }; + + // create the implementation + return std::make_unique(blockdev, std::move(header)); } + //------------------------------------------------- // impl ctor //------------------------------------------------- -coco_os9_impl::coco_os9_impl(fsblk_t &blockdev) +coco_os9_impl::coco_os9_impl(fsblk_t &blockdev, volume_header &&header) : filesystem_t(blockdev, 256) + , m_volume_header(std::move(header)) { } + +//------------------------------------------------- +// coco_os9_impl::volume_metadata +//------------------------------------------------- + +meta_data coco_os9_impl::volume_metadata() +{ + meta_data results; + results.set(meta_name::name, m_volume_header.name()); + results.set(meta_name::creation_date, m_volume_header.creation_date()); + return results; +} + + +//------------------------------------------------- +// coco_os9_impl::metadata +//------------------------------------------------- + +std::pair coco_os9_impl::metadata(const std::vector &path) +{ + // look up the path + std::optional header = find(path, { }); + if (!header) + return std::make_pair(ERR_NOT_FOUND, meta_data()); + + return std::make_pair(ERR_OK, header->metadata()); +} + + +//------------------------------------------------- +// coco_os9_impl::directory_contents +//------------------------------------------------- + +std::pair> coco_os9_impl::directory_contents(const std::vector &path) +{ + // look up the path + std::optional header = find(path, dir_entry_type::dir); + if (!header) + return std::make_pair(ERR_NOT_FOUND, std::vector()); + + // iterate through the directory + std::vector results; + auto callback = [this, &results](std::string &&filename, u32 lsn) + { + file_header header(m_blockdev.get(lsn), std::move(filename)); + dir_entry_type entry_type = header.is_directory() + ? dir_entry_type::dir + : dir_entry_type::file; + results.emplace_back(entry_type, header.metadata()); + return false; + }; + iterate_directory_entries(*header, callback); + + // and we're done + return std::make_pair(ERR_OK, std::move(results)); +} + + +//------------------------------------------------- +// coco_os9_impl::file_read +//------------------------------------------------- + +std::pair> coco_os9_impl::file_read(const std::vector &path) +{ + // look up the path + std::optional header = find(path, dir_entry_type::file); + if (!header) + return std::make_pair(ERR_NOT_FOUND, std::vector()); + + std::vector data = read_file_data(*header); + return std::make_pair(ERR_OK, std::move(data)); +} + + +//------------------------------------------------- +// coco_os9_impl::format +//------------------------------------------------- + +err_t coco_os9_impl::format(const meta_data &meta) +{ + // for some reason, the OS-9 world favored filling with 0xE5 + m_blockdev.fill(0xE5); + + // identify geometry info + u8 sectors = 18; // TODO - we need a definitive technique to get the floppy geometry + u8 heads = 1; // TODO - we need a definitive technique to get the floppy geometry + u16 sector_bytes = 256; // TODO - we need a definitive technique to get the floppy geometry + bool is_double_density = true; // TODO - we need a definitive technique to get the floppy geometry + u32 tracks = m_blockdev.block_count() / sectors / heads; + + // get attributes from metadata + std::string volume_title = meta.get_string(meta_name::name, "UNTITLED"); + util::arbitrary_datetime creation_datetime = meta.get_date(meta_name::creation_date); + auto [creation_os9date, creation_os9time] = to_os9_date(creation_datetime); + + u32 lsn_count = m_blockdev.block_count(); + u16 cluster_size = 1; + u16 owner_id = 1; + u16 disk_id = 1; + u8 attributes = 0; + u32 allocation_bitmap_bits = lsn_count / cluster_size; + u32 allocation_bitmap_lsns = (allocation_bitmap_bits / 8 + sector_bytes - 1) / sector_bytes; + u8 format_flags = ((heads > 1) ? 0x01 : 0x00) + | (is_double_density ? 0x02 : 0x00); + + // volume header + auto volume_header = m_blockdev.get(0); + volume_header.fill(0x00); + volume_header.w24b(0, lsn_count); // DD.TOT - total secctors + volume_header.w8(3, sectors); // DD.TKS - track size in sectors + volume_header.w16b(4, (allocation_bitmap_bits + 7) / 8); // DD.MAP - allocation bitmap in bytes + volume_header.w16b(6, cluster_size); // DD.BIT - cluster size + volume_header.w24b(8, 1 + allocation_bitmap_lsns); // DD.DIR - root directory LSN + volume_header.w16b(11, owner_id); // DD.OWN - owner ID + volume_header.w8(13, attributes); // DD.ATT - Dattributes + volume_header.w16b(14, disk_id); // DD.DSK - disk ID + volume_header.w8(16, format_flags); // DD.FMT - format flags + volume_header.w16b(17, sectors); // DD.SPT - sectors per track + volume_header.w24b(26, creation_os9date); // DD.DAT - date of creation + volume_header.w16b(29, creation_os9time); // DD.DAT - time of creation + volume_header.wstr(31, to_os9_string(volume_title, 32)); // DD.NAM - title + volume_header.w16b(103, sector_bytes / 256); // sector bytes + + // path descriptor options + volume_header.w8(0x3f + 0x00, 1); // device class + volume_header.w8(0x3f + 0x01, 1); // drive number + volume_header.w8(0x3f + 0x03, 0x20); // device type + volume_header.w8(0x3f + 0x04, 1); // density capability + volume_header.w16b(0x3f + 0x05, tracks); // number of tracks + volume_header.w8(0x3f + 0x07, heads); // number of sides + volume_header.w16b(0x3f + 0x09, sectors); // sectors per track + volume_header.w16b(0x3f + 0x0b, sectors); // sectors on track zero + volume_header.w8(0x3f + 0x0d, 3); // sector interleave factor + volume_header.w8(0x3f + 0x0e, 8); // default sectors per allocation + + // allocation bitmap + u32 total_allocated_sectors = 1 + allocation_bitmap_lsns + 1 + 8; + std::vector abblk_bytes; + abblk_bytes.resize(sector_bytes); + for (u32 i = 0; i < allocation_bitmap_lsns; i++) + { + for (u32 j = 0; j < sector_bytes; j++) + { + u32 pos = (i * sector_bytes + j) * 8; + if (pos + 8 < total_allocated_sectors) + abblk_bytes[j] = 0xFF; + else if (pos >= total_allocated_sectors) + abblk_bytes[j] = 0x00; + else + abblk_bytes[j] = ~((1 << (8 - total_allocated_sectors + pos)) - 1); + } + + auto abblk = m_blockdev.get(1 + i); + abblk.copy(0, abblk_bytes.data(), sector_bytes); + } + + // root directory header + auto roothdr_blk = m_blockdev.get(1 + allocation_bitmap_lsns); + roothdr_blk.fill(0x00); + roothdr_blk.w8(0x00, 0xBF); + roothdr_blk.w8(0x01, 0x00); + roothdr_blk.w8(0x02, 0x00); + roothdr_blk.w24b(0x03, creation_os9date); + roothdr_blk.w16b(0x06, creation_os9time); + roothdr_blk.w8(0x08, 0x01); + roothdr_blk.w8(0x09, 0x00); + roothdr_blk.w8(0x0A, 0x00); + roothdr_blk.w8(0x0B, 0x00); + roothdr_blk.w8(0x0C, 0x40); + roothdr_blk.w24b(0x0D, creation_os9date); + roothdr_blk.w24b(0x10, 1 + allocation_bitmap_lsns + 1); + roothdr_blk.w16b(0x13, 8); + + // root directory data + auto rootdata_blk = m_blockdev.get(1 + allocation_bitmap_lsns + 1); + rootdata_blk.fill(0x00); + rootdata_blk.w8(0x00, 0x2E); + rootdata_blk.w8(0x01, 0xAE); + rootdata_blk.w8(0x1F, 1 + allocation_bitmap_lsns); + rootdata_blk.w8(0x20, 0xAE); + rootdata_blk.w8(0x3F, 1 + allocation_bitmap_lsns); + return ERR_OK; +} + + +//------------------------------------------------- +// coco_os9_impl::find +//------------------------------------------------- + +std::optional coco_os9_impl::find(const std::vector &path, std::optional expected_entry_type) const +{ + u32 lsn = m_volume_header.root_dir_lsn(); + file_header current(m_blockdev.get(lsn), ""); + + // traverse the directory + for (const std::string &path_part : path) + { + // use iterate_directory_entries() to find this file part + std::optional child_lsn; + auto callback = [&child_lsn, &path_part](std::string &&child_filename, u32 this_child_lsn) + { + bool found = child_filename == path_part; + if (found) + child_lsn = this_child_lsn; + return found; + }; + iterate_directory_entries(current, callback); + + // did we find the child? + if (!child_lsn) + return { }; + current = file_header(m_blockdev.get(*child_lsn), std::string(path_part)); + } + + // ensure that we found the entry type we expect + dir_entry_type current_entry_type = current.is_directory() ? dir_entry_type::dir : dir_entry_type::file; + return !expected_entry_type || current_entry_type == *expected_entry_type + ? std::move(current) + : std::optional(); +} + + +//------------------------------------------------- +// coco_os9_impl::iterate_directory_entries +//------------------------------------------------- + +void coco_os9_impl::iterate_directory_entries(const file_header &header, const std::function callback) const +{ + // read the directory data + std::vector directory_data = read_file_data(header); + + // and assemble results + bool done = false; + int directory_count = directory_data.size() / 32; + for (int i = 0; !done && i < directory_count; i++) + { + // determine the filename + std::string_view raw_filename((const char *)&directory_data[i * 32], 29); + std::string filename = pick_os9_string(raw_filename); + if (is_ignored_filename(filename)) + continue; + + // set up the child header + u32 lsn = pick_integer_be(&directory_data[i * 32] + 29, 3); + + // invoke the callback + done = callback(std::move(filename), lsn); + } +} + + +//------------------------------------------------- +// coco_os9_impl::read_file_data +//------------------------------------------------- + +std::vector coco_os9_impl::read_file_data(const file_header &header) const +{ + // prep the vector to return the data from + std::vector data; + data.reserve(header.file_size()); + + int entry_count = header.get_sector_map_entry_count(); + for (int i = 0; i < entry_count; i++) + { + u32 start_lsn; + u16 count; + header.get_sector_map_entry(i, start_lsn, count); + + for (u32 lsn = start_lsn; lsn < start_lsn + count; lsn++) + { + auto block = m_blockdev.get(lsn); + size_t block_size = std::min(std::min(u32(m_volume_header.sector_size()), block.size()), header.file_size() - u32(data.size())); + for (auto i = 0; i < block_size; i++) + data.push_back(block.rodata()[i]); + } + } + return data; +} + + +//------------------------------------------------- +// pick_os9_string +//------------------------------------------------- + +std::string coco_os9_impl::pick_os9_string(std::string_view raw_string) +{ + // find the last NUL or high bit character + auto iter = std::find_if(raw_string.begin(), raw_string.end(), [](char ch) + { + return ch == '\0' || ch & 0x80; + }); + + // get the preliminary result + std::string result(raw_string.begin(), iter); + + // and add the final character if we have to + if (iter < raw_string.end() && *iter & 0x80) + result.append(1, *iter & 0x7F); + return result; + +} + + +//------------------------------------------------- +// to_os9_string +//------------------------------------------------- + +std::string coco_os9_impl::to_os9_string(std::string_view s, size_t length) +{ + std::string result(length, '\0'); + for (auto i = 0; i < std::min(length, s.size()); i++) + { + result[i] = (s[i] & 0x7F) + | (i == s.size() ? 0x80 : 0x00); + } + return result; +} + + +//------------------------------------------------- +// pick_integer_be +//------------------------------------------------- + +u32 coco_os9_impl::pick_integer_be(const u8 *data, int length) +{ + u32 result = 0; + for (int i = 0; i < length; i++) + result |= u32(data[length - i - 1]) << i * 8; + return result; +} + + +//------------------------------------------------- +// from_os9_date +//------------------------------------------------- + +util::arbitrary_datetime coco_os9_impl::from_os9_date(u32 os9_date, u16 os9_time) +{ + util::arbitrary_datetime dt; + memset(&dt, 0, sizeof(dt)); + dt.year = ((os9_date >> 16) & 0xFF) + 1900; + dt.month = (os9_date >> 8) & 0xFF; + dt.day_of_month = (os9_date >> 0) & 0xFF; + dt.hour = (os9_time >> 8) & 0xFF; + dt.minute = (os9_time >> 0) & 0xFF; + return dt; +} + + +//------------------------------------------------- +// to_os9_date +//------------------------------------------------- + +std::tuple coco_os9_impl::to_os9_date(const util::arbitrary_datetime &datetime) +{ + u32 os9_date = ((datetime.year - 1900) & 0xFF) << 16 + | (datetime.month & 0xFF) << 8 + | (datetime.day_of_month & 0xFF) << 0; + u16 os9_time = (datetime.hour & 0xFF) << 8 + | (datetime.minute & 0xFF) << 0; + return std::make_tuple(os9_date, os9_time); +} + + +//------------------------------------------------- +// validate_filename +//------------------------------------------------- + +bool coco_os9_impl::validate_filename(std::string_view name) +{ + return !is_ignored_filename(name) + && name.size() <= 29 + && std::find_if(name.begin(), name.end(), [](const char ch) { return ch == '\0' || ch == '/' || ch >= 0x80; }) == name.end(); +} + + //------------------------------------------------- // is_ignored_filename - should this file name be // ignored if it is in the file system? @@ -191,13 +669,92 @@ bool coco_os9_impl::is_ignored_filename(std::string_view name) || name == ".."; } + //------------------------------------------------- -// validate_filename +// volume_header ctor //------------------------------------------------- -bool coco_os9_impl::validate_filename(std::string_view name) +coco_os9_impl::volume_header::volume_header(fsblk_t::block_t &&block) + : m_block(std::move(block)) { - return !is_ignored_filename(name) - && name.size() <= 29 - && std::find_if(name.begin(), name.end(), [](const char ch) { return ch == '\0' || ch == '/' || ch >= 0x80; }) == name.end(); +} + + +//------------------------------------------------- +// volume_header::name +//------------------------------------------------- + +std::string coco_os9_impl::volume_header::name() const +{ + std::string_view raw_name((const char *)&m_block.rodata()[31], 32); + return pick_os9_string(raw_name); +} + + +//------------------------------------------------- +// file_header ctor +//------------------------------------------------- + +coco_os9_impl::file_header::file_header(fsblk_t::block_t &&block, std::string &&filename) + : m_block(std::move(block)) + , m_filename(std::move(filename)) +{ +} + + +//------------------------------------------------- +// file_header::creation_date +//------------------------------------------------- + +util::arbitrary_datetime coco_os9_impl::file_header::creation_date() const +{ + return from_os9_date(m_block.r24b(13)); +} + + +//------------------------------------------------- +// file_header::metadata +//------------------------------------------------- + +meta_data coco_os9_impl::file_header::metadata() const +{ + // format the attributes + std::string attributes = util::string_format("%c%c%c%c%c%c%c%c", + is_directory() ? 'd' : '-', + is_non_sharable() ? 's' : '-', + is_public_execute() ? 'x' : '-', + is_public_write() ? 'w' : '-', + is_public_read() ? 'r' : '-', + is_user_execute() ? 'x' : '-', + is_user_write() ? 'w' : '-', + is_user_read() ? 'r' : '-'); + + meta_data result; + result.set(meta_name::name, m_filename); + result.set(meta_name::creation_date, creation_date()); + result.set(meta_name::owner_id, owner_id()); + result.set(meta_name::attributes, std::move(attributes)); + result.set(meta_name::length, file_size()); + return result; +} + + +//------------------------------------------------- +// file_header::get_sector_map_entry_count +//------------------------------------------------- + +int coco_os9_impl::file_header::get_sector_map_entry_count() const +{ + return (m_block.size() - 16) / 5; +} + + +//------------------------------------------------- +// file_header::get_sector_map_entry +//------------------------------------------------- + +void coco_os9_impl::file_header::get_sector_map_entry(int entry_number, u32 &start_lsn, u16 &count) const +{ + start_lsn = m_block.r24b(16 + (entry_number * 5) + 0); + count = m_block.r16b(16 + (entry_number * 5) + 3); }