Add creeate, put and delete support to the CP/M filesystem driver.

This commit is contained in:
David Given
2024-05-13 00:32:57 +02:00
parent 1d1143a893
commit 78d5584e21
3 changed files with 427 additions and 24 deletions

View File

@@ -1,19 +1,24 @@
#include "lib/globals.h" #include "lib/globals.h"
#include "lib/vfs/vfs.h" #include "lib/vfs/vfs.h"
#include "lib/config.pb.h" #include "lib/config.pb.h"
#include <fmt/format.h>
#include <regex>
class CpmFsFilesystem : public Filesystem class CpmFsFilesystem : public Filesystem, public HasBitmap, public HasMount
{ {
class Entry class Entry
{ {
public: 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; user = bytes[0] & 0x0f;
{ {
std::stringstream ss; std::stringstream ss;
ss << (char)(user + '0') << ':';
for (int i = 1; i <= 8; i++) 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: public:
unsigned index;
std::string filename; std::string filename;
std::string mode; std::string mode;
unsigned user; unsigned user;
unsigned extent; unsigned extent;
unsigned records; unsigned records;
std::vector<unsigned> allocation_map; std::vector<unsigned> allocation_map;
bool deleted = false;
}; };
public: public:
@@ -83,7 +192,8 @@ public:
uint32_t capabilities() const override 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<std::string, std::string> getMetadata() override std::map<std::string, std::string> getMetadata() override
@@ -94,7 +204,7 @@ public:
for (int d = 0; d < _config.dir_entries(); d++) for (int d = 0; d < _config.dir_entries(); d++)
{ {
auto entry = getEntry(d); auto entry = getEntry(d);
if (!entry) if (entry->deleted)
continue; continue;
for (unsigned block : entry->allocation_map) for (unsigned block : entry->allocation_map)
@@ -112,6 +222,17 @@ public:
return attributes; 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 FilesystemStatus check() override
{ {
return FS_OK; return FS_OK;
@@ -127,15 +248,15 @@ public:
for (int d = 0; d < _config.dir_entries(); d++) for (int d = 0; d < _config.dir_entries(); d++)
{ {
auto entry = getEntry(d); auto entry = getEntry(d);
if (!entry) if (entry->deleted)
continue; continue;
auto& dirent = map[entry->filename]; auto& dirent = map[entry->combinedFilename()];
if (!dirent) if (!dirent)
{ {
dirent = std::make_unique<Dirent>(); dirent = std::make_unique<Dirent>();
dirent->path = {entry->filename}; dirent->filename = entry->combinedFilename();
dirent->filename = entry->filename; dirent->path = {dirent->filename};
dirent->mode = entry->mode; dirent->mode = entry->mode;
dirent->length = 0; dirent->length = 0;
dirent->file_type = TYPE_FILE; dirent->file_type = TYPE_FILE;
@@ -190,9 +311,9 @@ public:
for (int d = 0; d < _config.dir_entries(); d++) for (int d = 0; d < _config.dir_entries(); d++)
{ {
entry = getEntry(d); entry = getEntry(d);
if (!entry) if (entry->deleted)
continue; continue;
if (path[0] != entry->filename) if (path[0] != entry->combinedFilename())
continue; continue;
if (entry->extent < logicalExtent) if (entry->extent < logicalExtent)
continue; continue;
@@ -201,7 +322,7 @@ public:
break; break;
} }
if (!entry) if (entry->deleted)
{ {
if (logicalExtent == 0) if (logicalExtent == 0)
throw FileNotFoundException(); throw FileNotFoundException();
@@ -236,8 +357,116 @@ public:
return data; return data;
} }
private: public:
void mount() 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> entry = getEntry(d);
if (entry->deleted)
continue;
if (path[0] == entry->combinedFilename())
throw CannotWriteException();
}
/* Write blocks, one at a time. */
std::unique_ptr<Entry> 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(); auto& start = _config.filesystem_start();
_filesystemStart = _filesystemStart =
@@ -268,26 +497,71 @@ private:
_blocksPerLogicalExtent = 16384 / _config.block_size(); _blocksPerLogicalExtent = 16384 / _config.block_size();
_directory = getCpmBlock(0, _dirBlocks); _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> 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<Entry> getEntry(unsigned d) std::unique_ptr<Entry> getEntry(unsigned d)
{ {
auto bytes = _directory.slice(d * 32, 32); auto bytes = _directory.slice(d * 32, 32);
if (bytes[0] == 0xe5) return std::make_unique<Entry>(bytes, _allocationMapSize, d);
return nullptr;
return std::make_unique<Entry>(bytes, _allocationMapSize);
} }
Bytes getCpmBlock(uint32_t number, uint32_t count = 1) void putEntry(std::unique_ptr<Entry>& entry)
{ {
unsigned sector = number * _blockSectors; ByteWriter bw(_directory);
bw.seek(entry->index * 32);
bw.append(entry->toBytes(_allocationMapSize));
}
unsigned computeSector(uint32_t block) const
{
unsigned sector = block * _blockSectors;
if (_config.has_padding()) if (_config.has_padding())
sector += (sector / _config.padding().every()) * sector += (sector / _config.padding().every()) *
_config.padding().amount(); _config.padding().amount();
return sector;
}
Bytes getCpmBlock(uint32_t block, uint32_t count = 1)
{
return getLogicalSector( 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<bool> getBitmapForDebugging() override
{
return _bitmap;
} }
private: private:
@@ -303,6 +577,7 @@ private:
uint32_t _blocksPerLogicalExtent; uint32_t _blocksPerLogicalExtent;
int _allocationMapSize; int _allocationMapSize;
Bytes _directory; Bytes _directory;
std::vector<bool> _bitmap;
}; };
std::unique_ptr<Filesystem> Filesystem::createCpmFsFilesystem( std::unique_ptr<Filesystem> Filesystem::createCpmFsFilesystem(

View File

@@ -277,4 +277,18 @@ public:
static std::unique_ptr<Filesystem> createFilesystemFromConfig(); static std::unique_ptr<Filesystem> createFilesystemFromConfig();
}; };
/* Used for tests only. */
class HasBitmap
{
public:
virtual std::vector<bool> getBitmapForDebugging() = 0;
};
class HasMount
{
public:
virtual void mount() = 0;
};
#endif #endif

View File

@@ -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, static Bytes createDirent(const std::string& filename,
int extent, int extent,
int records, int records,
const std::initializer_list<int> blocks) const std::initializer_list<int> blocks,
int user = 0)
{ {
Bytes dirent; Bytes dirent;
ByteWriter bw(dirent); ByteWriter bw(dirent);
bw.write_8(0); bw.write_8(user);
bw.append(filename); bw.append(filename);
while (bw.pos != 12) while (bw.pos != 12)
bw.write_8(' '); bw.write_8(' ');
@@ -69,6 +77,21 @@ static void setBlock(
sectors->put(block, 0, i)->data = data.slice(i * 256, 256); sectors->put(block, 0, i)->data = data.slice(i * 256, 256);
} }
static Bytes getBlock(
const std::shared_ptr<SectorInterface>& 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() static void testPartialExtent()
{ {
auto sectors = std::make_shared<TestSectorInterface>(); auto sectors = std::make_shared<TestSectorInterface>();
@@ -113,6 +136,93 @@ static void testLogicalExtents()
AssertThat(data[0x4000 * 2], Equals(3)); AssertThat(data[0x4000 * 2], Equals(3));
} }
static void testBitmap()
{
auto sectors = std::make_shared<TestSectorInterface>();
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<HasMount*>(fs.get())->mount();
std::vector<bool> bitmap =
dynamic_cast<HasBitmap*>(fs.get())->getBitmapForDebugging();
AssertThat(bitmap,
Equals(std::vector<bool>{
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<TestSectorInterface>();
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<HasMount*>(fs.get())->mount();
std::vector<bool> bitmap =
dynamic_cast<HasBitmap*>(fs.get())->getBitmapForDebugging();
AssertThat(bitmap,
Equals(std::vector<bool>{
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<TestSectorInterface>();
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<TestSectorInterface>();
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) int main(void)
{ {
try try
@@ -124,7 +234,7 @@ int main(void)
layout { layout {
format_type: FORMATTYPE_80TRACK format_type: FORMATTYPE_80TRACK
tracks: 10 tracks: 20
sides: 1 sides: 1
layoutdata { layoutdata {
sector_size: 256 sector_size: 256
@@ -148,6 +258,10 @@ int main(void)
testPartialExtent(); testPartialExtent();
testLogicalExtents(); testLogicalExtents();
testBitmap();
testPutGet();
testPutBigFile();
testDelete();
} }
catch (const ErrorException& e) catch (const ErrorException& e)
{ {