From ea1ab029f3f58058d5a3fbc2e7394d16acb692fe Mon Sep 17 00:00:00 2001 From: David Given Date: Sun, 11 Sep 2022 13:19:15 +0200 Subject: [PATCH] Add a very untested ProDOS filesystem module. --- doc/filesystem.md | 1 + lib/build.mk | 1 + lib/vfs/prodos.cc | 309 ++++++++++++++++++++++++++++++++++++++++++ lib/vfs/vfs.cc | 10 +- lib/vfs/vfs.h | 2 + lib/vfs/vfs.proto | 3 + src/gui/mainwindow.cc | 5 +- 7 files changed, 327 insertions(+), 4 deletions(-) create mode 100644 lib/vfs/prodos.cc diff --git a/doc/filesystem.md b/doc/filesystem.md index f35fc77a..4e74f170 100644 --- a/doc/filesystem.md +++ b/doc/filesystem.md @@ -22,6 +22,7 @@ The following file systems are supported so far. | CP/M | Y | | For selected formats | | FatFS (a.k.a. MS-DOS) | Y | Y | FAT12, FAT16, FAT32, but only SFN; not Atari | | Macintosh HFS | Y | Y | Only AppleDouble files may be written | +| Apple ProDOS | Y | | | Please not that Atari disks do _not_ use standard FatFS, and the library I'm using for the heavy lifting (ChaN's diff --git a/lib/build.mk b/lib/build.mk index 2da9cb07..61feb8bc 100644 --- a/lib/build.mk +++ b/lib/build.mk @@ -73,6 +73,7 @@ LIBFLUXENGINE_SRCS = \ lib/vfs/cpmfs.cc \ lib/vfs/fatfs.cc \ lib/vfs/machfs.cc \ + lib/vfs/prodos.cc \ lib/vfs/vfs.cc \ lib/vfs/fluxsectorinterface.cc \ lib/vfs/imagesectorinterface.cc \ diff --git a/lib/vfs/prodos.cc b/lib/vfs/prodos.cc new file mode 100644 index 00000000..537b00d4 --- /dev/null +++ b/lib/vfs/prodos.cc @@ -0,0 +1,309 @@ +#include "lib/globals.h" +#include "lib/vfs/vfs.h" +#include "lib/config.pb.h" +#include + +/* This is described here: + * http://fileformats.archiveteam.org/wiki/ProDOS_file_system + */ + +class ProdosFilesystem : public Filesystem +{ + static constexpr int BLOCK_SECTORS = 2; + static constexpr int ROOT_DIRECTORY_BLOCK = 2; + + enum + { + STORAGETYPE_VOLUME = 0xf0, + STORAGETYPE_DIRBLOCK = 0xe0, + STORAGETYPE_SUBDIR = 0xd0, + STORAGETYPE_TREE = 0x30, + STORAGETYPE_SAPLING = 0x20, + STORAGETYPE_SEEDLING = 0x10 + }; + + class ProdosDirent : public Dirent + { + public: + ProdosDirent(const Bytes& de) + { + ByteReader br(de); + storageType = br.read_8(); + filename = br.read(15).slice(0, storageType & 0x0f); + storageType &= 0xf0; + prodosType = br.read_8(); + keyBlock = br.read_le16(); + blocksUsed = br.read_le16(); + length = br.read_le24(); + ctime = br.read_le32(); + version = br.read_8(); + minVersion = br.read_8(); + access = br.read_8(); + auxType = br.read_8(); + mtime = br.read_le32(); + + file_type = (storageType == STORAGETYPE_SUBDIR) ? TYPE_DIRECTORY + : TYPE_FILE; + + attributes[FILENAME] = filename; + attributes[LENGTH] = std::to_string(length); + attributes[FILE_TYPE] = + (file_type == TYPE_DIRECTORY) ? "dir" : "file"; + attributes[MODE] = ""; + attributes["prodos.storage_type"] = + fmt::format("0x{:x}", storageType); + attributes["prodos.prodos_type"] = std::to_string(prodosType); + attributes["prodos.key_block"] = std::to_string(keyBlock); + attributes["prodos.blocks_used"] = std::to_string(blocksUsed); + attributes["prodos.version"] = std::to_string(version); + attributes["prodos.min_version"] = std::to_string(minVersion); + attributes["prodos.access"] = std::to_string(access); + attributes["prodos.aux_type"] = std::to_string(auxType); + } + + uint8_t storageType; + uint8_t prodosType; + uint16_t keyBlock; + uint16_t blocksUsed; + uint32_t ctime; + uint8_t version; + uint8_t minVersion; + uint8_t access; + uint8_t auxType; + uint32_t mtime; + }; + + class Directory + { + public: + Directory(ProdosFilesystem* fs, uint16_t block): _fs(fs), _block(block) + { + for (;;) + { + block = readDirectoryBlock(block); + if (!block) + break; + } + } + + public: + std::shared_ptr find(const std::string& filename) + { + for (auto& dirent : dirents) + if (dirent->filename == filename) + return dirent; + + throw FileNotFoundException(); + } + + private: + uint16_t readDirectoryBlock(uint16_t block) + { + auto bytes = _fs->getLogicalBlock(block); + ByteReader br(bytes); + br.seek(2); + uint16_t nextBlock = br.read_le16(); + + int here; + if ((br.read_8() & 0xf0) >= 0xe0) + { + /* First block of a directory. */ + br.seek(0x2b); + } + else + br.seek(4); + + while (br.pos < 473) + { + Bytes de = br.read(0x27); + if ((de[0] & 0xf0) == 0) + continue; + + dirents.push_back(std::make_shared(de)); + } + + return nextBlock; + } + + private: + ProdosFilesystem* _fs; + uint16_t _block; + + public: + std::vector> dirents; + }; + +public: + ProdosFilesystem( + const ProdosProto& config, std::shared_ptr sectors): + Filesystem(sectors), + _config(config) + { + } + + uint32_t capabilities() const + { + return OP_LIST | OP_GETDIRENT | OP_GETFILE | OP_GETFSDATA; + } + + std::map getMetadata() override + { + mount(); + + auto block = getLogicalBlock(ROOT_DIRECTORY_BLOCK); + + uint8_t flen = block[4] & 0x0f; + std::string volumename = block.slice(5, flen); + + uint16_t usedBlocks = 0; + for (bool bit : _allocationBitmap) + if (!bit) + usedBlocks++; + + std::map attributes; + attributes[VOLUME_NAME] = volumename; + attributes[TOTAL_BLOCKS] = + std::to_string(block.reader().seek(0x29).read_le16()); + attributes[USED_BLOCKS] = std::to_string(usedBlocks); + attributes[BLOCK_SIZE] = "512"; + return attributes; + } + + std::vector> list(const Path& path) override + { + mount(); + + auto dir = chdir(path); + std::vector> results; + for (auto& de : dir->dirents) + results.push_back(de); + + return results; + } + + std::shared_ptr getDirent(const Path& path) override + { + mount(); + + auto dir = chdir(path.parent()); + return dir->find(path.back()); + } + + Bytes getFile(const Path& path) override + { + mount(); + + auto dir = chdir(path.parent()); + auto dirent = dir->find(path.back()); + + Bytes bytes; + switch (dirent->storageType) + { + case STORAGETYPE_SUBDIR: + throw BadPathException("tried to use a directory like a file"); + + case STORAGETYPE_SEEDLING: + bytes = getLogicalBlock(dirent->keyBlock); + break; + + case STORAGETYPE_SAPLING: + { + auto keyBytes = getLogicalBlock(dirent->keyBlock); + ByteWriter bw(bytes); + readIndexBlock(keyBytes, bw); + break; + } + + case STORAGETYPE_TREE: + { + auto masterKeyBytes = getLogicalBlock(dirent->keyBlock); + ByteWriter bw(bytes); + ByteReader br(masterKeyBytes); + + /* This always appends 16MB of data, the maximum amount + * for a tree file, which is wasteful but simple. + */ + + while (!br.eof()) + { + uint16_t indexBlock = br.read_le16(); + if (indexBlock) + readIndexBlock(getLogicalBlock(indexBlock), bw); + else + bw += Bytes(128 * 1024); + } + break; + } + + default: + throw UnimplementedFilesystemException( + fmt::format("storage type 0x{:x} isn't supported yet", + dirent->storageType)); + } + + return bytes.slice(0, dirent->length); + } + +private: + void mount() + { + auto rootVolume = getLogicalBlock(ROOT_DIRECTORY_BLOCK); + if ((rootVolume[4] & 0xf0) != 0xf0) + throw BadFilesystemException(); + + ByteReader br(rootVolume); + br.seek(0x27); + _allocationBitmapLocation = br.read_le16(); + uint16_t totalBlocks = br.read_le16(); + + uint16_t bitmapBlocks = (totalBlocks + 4095) / 4096; + _allocationBitmap = + getLogicalBlock(_allocationBitmapLocation, bitmapBlocks).toBits(); + _allocationBitmap.resize(totalBlocks); + } + + std::unique_ptr chdir(const Path& path) + { + std::unique_ptr dir = + std::make_unique(this, ROOT_DIRECTORY_BLOCK); + for (const auto& element : path) + { + auto entry = dir->find(element); + if (entry->file_type == TYPE_FILE) + throw BadPathException("tried to use a file like a directory"); + + dir = std::make_unique(this, entry->keyBlock); + } + return dir; + } + + /* Always appends 128kB of data. */ + void readIndexBlock(const Bytes& indexBlock, ByteWriter& bw) + { + ByteReader br(indexBlock); + while (!br.eof()) + { + uint16_t block = br.read_le16(); + if (block) + bw += getLogicalBlock(block); + else + bw += Bytes(512); + } + } + + Bytes getLogicalBlock(uint16_t block, unsigned count = 1) + { + return getLogicalSector(block * BLOCK_SECTORS, count * BLOCK_SECTORS); + } + +private: + const ProdosProto& _config; + uint16_t _allocationBitmapLocation; + std::vector _allocationBitmap; +}; + +std::unique_ptr Filesystem::createProdosFilesystem( + const FilesystemProto& config, std::shared_ptr sectors) +{ + return std::make_unique(config.prodos(), sectors); +} diff --git a/lib/vfs/vfs.cc b/lib/vfs/vfs.cc index 58fe42a8..e5d30ac8 100644 --- a/lib/vfs/vfs.cc +++ b/lib/vfs/vfs.cc @@ -200,6 +200,9 @@ std::unique_ptr Filesystem::createFilesystem( case FilesystemProto::kCbmfs: return Filesystem::createCbmfsFilesystem(config, image); + case FilesystemProto::kProdos: + return Filesystem::createProdosFilesystem(config, image); + default: Error() << "no filesystem configured"; return std::unique_ptr(); @@ -258,7 +261,9 @@ Bytes Filesystem::getSector(unsigned track, unsigned side, unsigned sector) Bytes Filesystem::getLogicalSector(uint32_t number, uint32_t count) { if ((number + count) > _locations.size()) - throw BadFilesystemException(); + throw BadFilesystemException( + fmt::format("invalid filesystem: sector {} is out of bounds", + number + count - 1)); Bytes data; ByteWriter bw(data); @@ -278,7 +283,8 @@ Bytes Filesystem::getLogicalSector(uint32_t number, uint32_t count) void Filesystem::putLogicalSector(uint32_t number, const Bytes& data) { if (number >= _locations.size()) - throw BadFilesystemException(); + throw BadFilesystemException(fmt::format( + "invalid filesystem: sector {} is out of bounds", number)); unsigned pos = 0; while (pos < data.size()) diff --git a/lib/vfs/vfs.h b/lib/vfs/vfs.h index f96d2898..697c257f 100644 --- a/lib/vfs/vfs.h +++ b/lib/vfs/vfs.h @@ -240,6 +240,8 @@ public: const FilesystemProto& config, std::shared_ptr image); static std::unique_ptr createCbmfsFilesystem( const FilesystemProto& config, std::shared_ptr image); + static std::unique_ptr createProdosFilesystem( + const FilesystemProto& config, std::shared_ptr image); static std::unique_ptr createFilesystem( const FilesystemProto& config, std::shared_ptr image); diff --git a/lib/vfs/vfs.proto b/lib/vfs/vfs.proto index d54d802b..6f4be8fe 100644 --- a/lib/vfs/vfs.proto +++ b/lib/vfs/vfs.proto @@ -56,6 +56,8 @@ message CbmfsProto ]; } +message ProdosProto {} + message FilesystemProto { oneof filesystem @@ -67,5 +69,6 @@ message FilesystemProto AmigaFfsProto amigaffs = 5; MacHfsProto machfs = 6; CbmfsProto cbmfs = 7; + ProdosProto prodos = 8; } } diff --git a/src/gui/mainwindow.cc b/src/gui/mainwindow.cc index 239201ae..b2b7e9bc 100644 --- a/src/gui/mainwindow.cc +++ b/src/gui/mainwindow.cc @@ -258,9 +258,10 @@ public: runOnWorkerThread( [this]() { - /* You need to call this if the config changes to invalidate any caches. */ + /* You need to call this if the config changes to invalidate + * any caches. */ - Environment::reset(); + Environment::reset(); auto fluxSource = FluxSource::create(config.flux_source()); auto decoder = AbstractDecoder::create(config.decoder());