diff --git a/lib/vfs/cpmfs.cc b/lib/vfs/cpmfs.cc index e3da4f69..90649aa8 100644 --- a/lib/vfs/cpmfs.cc +++ b/lib/vfs/cpmfs.cc @@ -1,19 +1,24 @@ #include "lib/globals.h" #include "lib/vfs/vfs.h" #include "lib/config.pb.h" +#include +#include -class CpmFsFilesystem : public Filesystem +class CpmFsFilesystem : public Filesystem, public HasBitmap, public HasMount { class Entry { public: - Entry(const Bytes& bytes, int map_entry_size) + Entry(const Bytes& bytes, int map_entry_size, unsigned index): + index(index) { + if (bytes[0] == 0xe5) + deleted = true; + user = bytes[0] & 0x0f; { std::stringstream ss; - ss << (char)(user + '0') << ':'; for (int i = 1; i <= 8; i++) { @@ -64,13 +69,117 @@ class CpmFsFilesystem : public Filesystem } } + Bytes toBytes(int map_entry_size) const + { + Bytes bytes(32); + ByteWriter bw(bytes); + + if (deleted) + { + for (int i = 0; i < 32; i++) + bw.write_8(0xe5); + } + else + { + bw.write_8(user); + + /* Encode the filename. */ + + for (int i = 1; i < 12; i++) + bytes[i] = 0x20; + for (char c : filename) + { + if (islower(c)) + throw BadPathException(); + if (c == '.') + { + if (bw.pos >= 9) + throw BadPathException(); + bw.seek(9); + continue; + } + if ((bw.pos == 9) || (bw.pos == 12)) + throw BadPathException(); + bw.write_8(c); + } + + /* Set the mode. */ + + if (mode.find('R') != std::string::npos) + bytes[9] |= 0x80; + if (mode.find('S') != std::string::npos) + bytes[10] |= 0x80; + if (mode.find('A') != std::string::npos) + bytes[11] |= 0x80; + + /* EX, S1, S2, RC */ + + bw.seek(12); + bw.write_8(extent & 0x1f); /* EX */ + bw.write_8(0); /* S1 */ + bw.write_8(extent >> 5); /* S2 */ + bw.write_8(records); /* RC */ + + /* Allocation map. */ + + switch (map_entry_size) + { + case 1: + for (int i = 0; i < 16; i++) + bw.write_8(allocation_map[i]); + break; + + case 2: + for (int i = 0; i < 8; i++) + bw.write_le16(allocation_map[i]); + break; + } + } + + return bytes; + } + + void changeFilename(const std::string& filename) + { + static std::regex FORMATTER("(?:(1?[0-9]):)?([^ .]+)\\.?([^ .]*)"); + std::smatch results; + bool matched = std::regex_match(filename, results, FORMATTER); + if (!matched) + throw BadPathException(); + + std::string user = results[1]; + std::string stem = results[2]; + std::string ext = results[3]; + + if (stem.size() > 8) + throw BadPathException(); + if (ext.size() > 3) + throw BadPathException(); + + this->user = std::stoi(user); + if (this->user > 15) + throw BadPathException(); + + if (ext.empty()) + this->filename = stem; + else + this->filename = fmt::format("{}.{}", stem, ext); + } + + std::string combinedFilename() const + { + return fmt::format("{}:{}", user, filename); + } + public: + unsigned index; std::string filename; std::string mode; unsigned user; unsigned extent; unsigned records; std::vector allocation_map; + bool deleted = false; }; public: @@ -83,7 +192,8 @@ class CpmFsFilesystem : public Filesystem uint32_t capabilities() const override { - return OP_GETFSDATA | OP_LIST | OP_GETFILE | OP_GETDIRENT; + return OP_GETFSDATA | OP_LIST | OP_GETFILE | OP_PUTFILE | OP_DELETE | + OP_GETDIRENT | OP_CREATE; } std::map getMetadata() override @@ -94,7 +204,7 @@ class CpmFsFilesystem : public Filesystem for (int d = 0; d < _config.dir_entries(); d++) { auto entry = getEntry(d); - if (!entry) + if (entry->deleted) continue; for (unsigned block : entry->allocation_map) @@ -112,6 +222,17 @@ class CpmFsFilesystem : public Filesystem return attributes; } + void create(bool, const std::string&) override + { + auto& start = _config.filesystem_start(); + _filesystemStart = + getOffsetOfSector(start.track(), start.side(), start.sector()); + _sectorSize = getLogicalSectorSize(start.track(), start.side()); + + _directory = Bytes{0xe5} * (_config.dir_entries() * 32); + putCpmBlock(0, _directory); + } + FilesystemStatus check() override { return FS_OK; @@ -127,15 +248,15 @@ class CpmFsFilesystem : public Filesystem for (int d = 0; d < _config.dir_entries(); d++) { auto entry = getEntry(d); - if (!entry) + if (entry->deleted) continue; - auto& dirent = map[entry->filename]; + auto& dirent = map[entry->combinedFilename()]; if (!dirent) { dirent = std::make_unique(); - dirent->path = {entry->filename}; - dirent->filename = entry->filename; + dirent->filename = entry->combinedFilename(); + dirent->path = {dirent->filename}; dirent->mode = entry->mode; dirent->length = 0; dirent->file_type = TYPE_FILE; @@ -190,9 +311,9 @@ class CpmFsFilesystem : public Filesystem for (int d = 0; d < _config.dir_entries(); d++) { entry = getEntry(d); - if (!entry) + if (entry->deleted) continue; - if (path[0] != entry->filename) + if (path[0] != entry->combinedFilename()) continue; if (entry->extent < logicalExtent) continue; @@ -201,7 +322,7 @@ class CpmFsFilesystem : public Filesystem break; } - if (!entry) + if (entry->deleted) { if (logicalExtent == 0) throw FileNotFoundException(); @@ -236,8 +357,116 @@ class CpmFsFilesystem : public Filesystem return data; } -private: - void mount() +public: + void putFile(const Path& path, const Bytes& bytes) override + { + mount(); + if (path.size() != 1) + throw BadPathException(); + + /* Test to see if the file already exists. */ + + for (int d = 0; d < _config.dir_entries(); d++) + { + std::unique_ptr entry = getEntry(d); + if (entry->deleted) + continue; + if (path[0] == entry->combinedFilename()) + throw CannotWriteException(); + } + + /* Write blocks, one at a time. */ + + std::unique_ptr entry; + ByteReader br(bytes); + while (!br.eof()) + { + unsigned extent = br.pos / 0x4000; + Bytes block = br.read(_config.block_size()); + + /* Allocate a block and write it. */ + + auto bit = std::find(_bitmap.begin(), _bitmap.end(), false); + if (bit == _bitmap.end()) + throw DiskFullException(); + *bit = true; + unsigned blocknum = bit - _bitmap.begin(); + putCpmBlock(blocknum, block); + + /* Do we need a new directory entry? */ + + if (!entry || + entry->allocation_map[std::size(entry->allocation_map) - 1]) + { + if (entry) + { + entry->records = 0x80; + putEntry(entry); + } + + entry.reset(); + for (int d = 0; d < _config.dir_entries(); d++) + { + entry = getEntry(d); + if (entry->deleted) + break; + entry.reset(); + } + + if (!entry) + throw DiskFullException(); + entry->deleted = false; + entry->changeFilename(path[0]); + entry->extent = extent; + entry->mode = ""; + std::fill(entry->allocation_map.begin(), + entry->allocation_map.end(), + 0); + } + + /* Hook up the block in the allocation map. */ + + auto mit = std::find( + entry->allocation_map.begin(), entry->allocation_map.end(), 0); + *mit = blocknum; + } + if (entry) + { + entry->records = ((bytes.size() & 0x3fff) + 127) / 128; + putEntry(entry); + } + + unmount(); + } + + void deleteFile(const Path& path) override + { + mount(); + if (path.size() != 1) + throw BadPathException(); + + /* Remove all dirents for this file. */ + + bool found = false; + for (int d = 0; d < _config.dir_entries(); d++) + { + auto entry = getEntry(d); + if (entry->deleted) + continue; + if (path[0] != entry->combinedFilename()) + continue; + entry->deleted = true; + putEntry(entry); + found = true; + } + + if (!found) + throw FileNotFoundException(); + unmount(); + } + +public: + void mount() override { auto& start = _config.filesystem_start(); _filesystemStart = @@ -268,26 +497,71 @@ class CpmFsFilesystem : public Filesystem _blocksPerLogicalExtent = 16384 / _config.block_size(); _directory = getCpmBlock(0, _dirBlocks); + + /* Create the allocation bitmap. */ + + _bitmap.clear(); + _bitmap.resize(_filesystemBlocks); + for (int d = 0; d < _dirBlocks; d++) + _bitmap[d] = true; + for (int d = 0; d < _config.dir_entries(); d++) + { + std::unique_ptr entry = getEntry(d); + if (entry->deleted) + continue; + for (unsigned block : entry->allocation_map) + { + if (block >= _filesystemBlocks) + throw BadFilesystemException(); + if (block) + _bitmap[block] = true; + } + } } + void unmount() + { + putCpmBlock(0, _directory); + } + +private: std::unique_ptr getEntry(unsigned d) { auto bytes = _directory.slice(d * 32, 32); - if (bytes[0] == 0xe5) - return nullptr; + return std::make_unique(bytes, _allocationMapSize, d); + } - return std::make_unique(bytes, _allocationMapSize); + void putEntry(std::unique_ptr& entry) + { + ByteWriter bw(_directory); + bw.seek(entry->index * 32); + bw.append(entry->toBytes(_allocationMapSize)); } - Bytes getCpmBlock(uint32_t number, uint32_t count = 1) + unsigned computeSector(uint32_t block) const { - unsigned sector = number * _blockSectors; + unsigned sector = block * _blockSectors; if (_config.has_padding()) sector += (sector / _config.padding().every()) * _config.padding().amount(); + return sector; + } + Bytes getCpmBlock(uint32_t block, uint32_t count = 1) + { return getLogicalSector( - sector + _filesystemStart, _blockSectors * count); + computeSector(block) + _filesystemStart, _blockSectors * count); + } + + void putCpmBlock(uint32_t block, const Bytes& bytes) + { + putLogicalSector(computeSector(block) + _filesystemStart, bytes); + } + +public: + std::vector getBitmapForDebugging() override + { + return _bitmap; } private: @@ -303,6 +577,7 @@ class CpmFsFilesystem : public Filesystem uint32_t _blocksPerLogicalExtent; int _allocationMapSize; Bytes _directory; + std::vector _bitmap; }; std::unique_ptr Filesystem::createCpmFsFilesystem( diff --git a/lib/vfs/vfs.h b/lib/vfs/vfs.h index 575959be..84478351 100644 --- a/lib/vfs/vfs.h +++ b/lib/vfs/vfs.h @@ -277,4 +277,18 @@ class Filesystem static std::unique_ptr createFilesystemFromConfig(); }; +/* Used for tests only. */ + +class HasBitmap +{ +public: + virtual std::vector getBitmapForDebugging() = 0; +}; + +class HasMount +{ +public: + virtual void mount() = 0; +}; + #endif diff --git a/tests/cpmfs.cc b/tests/cpmfs.cc index 7d9edf84..8ad3e066 100644 --- a/tests/cpmfs.cc +++ b/tests/cpmfs.cc @@ -37,14 +37,22 @@ namespace }; } +static std::ostream& operator<<(std::ostream& stream, const Bytes& bytes) +{ + stream << '\n'; + hexdump(stream, bytes); + return stream; +} + static Bytes createDirent(const std::string& filename, int extent, int records, - const std::initializer_list blocks) + const std::initializer_list blocks, + int user = 0) { Bytes dirent; ByteWriter bw(dirent); - bw.write_8(0); + bw.write_8(user); bw.append(filename); while (bw.pos != 12) bw.write_8(' '); @@ -69,6 +77,21 @@ static void setBlock( sectors->put(block, 0, i)->data = data.slice(i * 256, 256); } +static Bytes getBlock( + const std::shared_ptr& sectors, int block, int length) +{ + Bytes bytes; + ByteWriter bw(bytes); + + for (int i = 0; i < (length + 127) / 128; i++) + { + auto sector = sectors->get(block, 0, i); + bw.append(sector->data); + } + + return bytes; +} + static void testPartialExtent() { auto sectors = std::make_shared(); @@ -113,6 +136,93 @@ static void testLogicalExtents() AssertThat(data[0x4000 * 2], Equals(3)); } +static void testBitmap() +{ + auto sectors = std::make_shared(); + auto fs = Filesystem::createCpmFsFilesystem( + globalConfig()->filesystem(), sectors); + + setBlock(sectors, + 0, + createDirent("FILE", 1, 128, {1, 0, 0, 0, 0, 0, 0, 0, 2}) + + createDirent("FILE", 2, 128, {4}) + (blank_dirent * 62)); + + dynamic_cast(fs.get())->mount(); + std::vector bitmap = + dynamic_cast(fs.get())->getBitmapForDebugging(); + AssertThat(bitmap, + Equals(std::vector{ + 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); +} + +static void testPutGet() +{ + auto sectors = std::make_shared(); + auto fs = Filesystem::createCpmFsFilesystem( + globalConfig()->filesystem(), sectors); + fs->create(true, "volume"); + + fs->putFile(Path("0:FILE1"), Bytes{1, 2, 3, 4}); + fs->putFile(Path("0:FILE2"), Bytes{5, 6, 7, 8}); + + dynamic_cast(fs.get())->mount(); + std::vector bitmap = + dynamic_cast(fs.get())->getBitmapForDebugging(); + AssertThat(bitmap, + Equals(std::vector{ + 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); + + auto directory = getBlock(sectors, 0, 256).slice(0, 64); + AssertThat(directory, + Equals(createDirent("FILE1", 0, 1, {1}) + + createDirent("FILE2", 0, 1, {2}))); + + auto file1 = getBlock(sectors, 1, 8).slice(0, 8); + AssertThat(file1, Equals(Bytes{1, 2, 3, 4, 0, 0, 0, 0})); + + auto file2 = getBlock(sectors, 2, 8).slice(0, 8); + AssertThat(file2, Equals(Bytes{5, 6, 7, 8, 0, 0, 0, 0})); +} + +static void testPutBigFile() +{ + auto sectors = std::make_shared(); + auto fs = Filesystem::createCpmFsFilesystem( + globalConfig()->filesystem(), sectors); + fs->create(true, "volume"); + + Bytes filedata; + ByteWriter bw(filedata); + while (filedata.size() < 0x9000) + bw.write_le32(bw.pos); + + fs->putFile(Path("0:BIGFILE"), filedata); + + auto directory = getBlock(sectors, 0, 256).slice(0, 64); + AssertThat(directory, + Equals(createDirent("BIGFILE", + 0, + 0x80, + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}) + + createDirent("BIGFILE", 2, 0x20, {17, 18}))); +} + +static auto testDelete() +{ + auto sectors = std::make_shared(); + auto fs = Filesystem::createCpmFsFilesystem( + globalConfig()->filesystem(), sectors); + fs->create(true, "volume"); + + fs->putFile(Path("0:FILE1"), Bytes{1, 2, 3, 4}); + fs->putFile(Path("0:FILE2"), Bytes{5, 6, 7, 8}); + fs->deleteFile(Path("0:FILE1")); + + auto directory = getBlock(sectors, 0, 256).slice(0, 64); + AssertThat(directory, + Equals((Bytes{0xe5} * 32) + createDirent("FILE2", 0, 1, {2}))); +} + int main(void) { try @@ -124,7 +234,7 @@ int main(void) layout { format_type: FORMATTYPE_80TRACK - tracks: 10 + tracks: 20 sides: 1 layoutdata { sector_size: 256 @@ -148,6 +258,10 @@ int main(void) testPartialExtent(); testLogicalExtents(); + testBitmap(); + testPutGet(); + testPutBigFile(); + testDelete(); } catch (const ErrorException& e) {