Block device peripheral and SBFS file system implemented.
This commit is contained in:
@@ -0,0 +1,555 @@
|
||||
// SplitDisk.c
|
||||
// Makes and edits SplitBit disk images from the host.
|
||||
//
|
||||
// Until SplitBit can write its own filesystem there has to be some way to get a program
|
||||
// onto a disk, and this is it. It is not a shortcut around the machine: it speaks exactly
|
||||
// the format SplitBit will speak, so an image this makes is one the machine can read and
|
||||
// an image the machine writes is one this can read back.
|
||||
//
|
||||
// Written by Anachronaut
|
||||
|
||||
#include "sbfs.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// Numbers on a SplitBit disk are most significant byte first, the same as everywhere
|
||||
// else on the machine.
|
||||
static uint16_t readWord(const uint8_t *at) {
|
||||
return (uint16_t)((at[0] << 8) | at[1]);
|
||||
}
|
||||
|
||||
static void writeWord(uint8_t *at, uint16_t value) {
|
||||
at[0] = (uint8_t)(value >> 8);
|
||||
at[1] = (uint8_t)(value & 0xFF);
|
||||
}
|
||||
|
||||
static FILE *openImage(const char *path, const char *mode) {
|
||||
FILE *image = fopen(path, mode);
|
||||
if (image == NULL) {
|
||||
fprintf(stderr, "Error: Couldn't open the disk image \"%s\".\n", path);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
static int readBlock(FILE *image, uint16_t block, uint8_t *into) {
|
||||
if (fseek(image, (long)block * SBFS_BLOCK_BYTES, SEEK_SET) != 0
|
||||
|| fread(into, 1, SBFS_BLOCK_BYTES, image) != SBFS_BLOCK_BYTES) {
|
||||
fprintf(stderr, "Error: Couldn't read block %u.\n", block);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int writeBlock(FILE *image, uint16_t block, const uint8_t *from) {
|
||||
if (fseek(image, (long)block * SBFS_BLOCK_BYTES, SEEK_SET) != 0
|
||||
|| fwrite(from, 1, SBFS_BLOCK_BYTES, image) != SBFS_BLOCK_BYTES) {
|
||||
fprintf(stderr, "Error: Couldn't write block %u.\n", block);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
uint16_t diskBlocks;
|
||||
uint16_t directoryStart;
|
||||
uint16_t directoryBlocks;
|
||||
uint16_t freeBlocks;
|
||||
} Superblock;
|
||||
|
||||
// Reads block 0 and checks it really is one of ours. Without the magic a blank image and
|
||||
// a formatted one with no files would be the same thing.
|
||||
static int readSuperblock(FILE *image, Superblock *super) {
|
||||
uint8_t block[SBFS_BLOCK_BYTES];
|
||||
if (readBlock(image, 0, block)) {
|
||||
return 1;
|
||||
}
|
||||
if (memcmp(block, SBFS_MAGIC, SBFS_MAGIC_BYTES) != 0) {
|
||||
fprintf(stderr, "Error: That is not a SplitBit disk. Format it first.\n");
|
||||
return 1;
|
||||
}
|
||||
if (block[SBFS_SUPER_VERSION] != SBFS_VERSION) {
|
||||
fprintf(stderr, "Error: That disk is version %u, and this understands version %u.\n",
|
||||
block[SBFS_SUPER_VERSION], SBFS_VERSION);
|
||||
return 1;
|
||||
}
|
||||
super->diskBlocks = readWord(block + SBFS_SUPER_DISK);
|
||||
super->directoryStart = readWord(block + SBFS_SUPER_DIRSTART);
|
||||
super->directoryBlocks = readWord(block + SBFS_SUPER_DIRBLOCKS);
|
||||
super->freeBlocks = readWord(block + SBFS_SUPER_FREE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int writeSuperblock(FILE *image, const Superblock *super) {
|
||||
uint8_t block[SBFS_BLOCK_BYTES];
|
||||
memset(block, 0, sizeof(block));
|
||||
memcpy(block, SBFS_MAGIC, SBFS_MAGIC_BYTES);
|
||||
block[SBFS_SUPER_VERSION] = SBFS_VERSION;
|
||||
writeWord(block + SBFS_SUPER_DISK, super->diskBlocks);
|
||||
writeWord(block + SBFS_SUPER_DIRSTART, super->directoryStart);
|
||||
writeWord(block + SBFS_SUPER_DIRBLOCKS, super->directoryBlocks);
|
||||
writeWord(block + SBFS_SUPER_FREE, super->freeBlocks);
|
||||
return writeBlock(image, 0, block);
|
||||
}
|
||||
|
||||
// The whole directory, held in memory while a command works on it. It is small enough
|
||||
// that reading it once and writing it back once is simpler than picking at blocks, and it
|
||||
// is what the machine will do too once it has the Data Memory to spare.
|
||||
typedef struct {
|
||||
uint8_t *bytes;
|
||||
int entries;
|
||||
} Directory;
|
||||
|
||||
static int readDirectory(FILE *image, const Superblock *super, Directory *directory) {
|
||||
directory->entries = super->directoryBlocks * SBFS_ENTRIES_PER_BLOCK;
|
||||
directory->bytes = malloc((size_t)super->directoryBlocks * SBFS_BLOCK_BYTES);
|
||||
if (directory->bytes == NULL) {
|
||||
fprintf(stderr, "Error: Out of memory reading the directory.\n");
|
||||
return 1;
|
||||
}
|
||||
for (uint16_t i = 0; i < super->directoryBlocks; i++) {
|
||||
if (readBlock(image, (uint16_t)(super->directoryStart + i),
|
||||
directory->bytes + (size_t)i * SBFS_BLOCK_BYTES)) {
|
||||
free(directory->bytes);
|
||||
directory->bytes = NULL;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int writeDirectory(FILE *image, const Superblock *super, const Directory *directory) {
|
||||
for (uint16_t i = 0; i < super->directoryBlocks; i++) {
|
||||
if (writeBlock(image, (uint16_t)(super->directoryStart + i),
|
||||
directory->bytes + (size_t)i * SBFS_BLOCK_BYTES)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint8_t *entryAt(const Directory *directory, int index) {
|
||||
return directory->bytes + (size_t)index * SBFS_ENTRY_BYTES;
|
||||
}
|
||||
|
||||
static int entryInUse(const uint8_t *entry) {
|
||||
return (entry[SBFS_ENTRY_FLAGS] & SBFS_FLAG_IN_USE) != 0;
|
||||
}
|
||||
|
||||
static uint32_t entrySize(const uint8_t *entry) {
|
||||
return (uint32_t)readWord(entry + SBFS_ENTRY_BLOCKS) * SBFS_BLOCK_BYTES
|
||||
+ entry[SBFS_ENTRY_TAIL];
|
||||
}
|
||||
|
||||
static uint16_t entryBlocksUsed(const uint8_t *entry) {
|
||||
return (uint16_t)SBFS_BLOCKS_USED(readWord(entry + SBFS_ENTRY_BLOCKS),
|
||||
entry[SBFS_ENTRY_TAIL]);
|
||||
}
|
||||
|
||||
// Names are compared as written, the way labels are, and are padded rather than
|
||||
// terminated, so a name that fills the field has no terminator to find.
|
||||
static void entryName(const uint8_t *entry, char *into) {
|
||||
memcpy(into, entry + SBFS_ENTRY_NAME, SBFS_NAME_BYTES);
|
||||
into[SBFS_NAME_BYTES] = '\0';
|
||||
}
|
||||
|
||||
static int findByName(const Directory *directory, const char *name) {
|
||||
char held[SBFS_NAME_BYTES + 1];
|
||||
for (int i = 0; i < directory->entries; i++) {
|
||||
const uint8_t *entry = entryAt(directory, i);
|
||||
if (!entryInUse(entry)) {
|
||||
continue;
|
||||
}
|
||||
entryName(entry, held);
|
||||
if (strcmp(held, name) == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Where the first free run of the wanted length begins, or -1 if there is not one.
|
||||
//
|
||||
// There is no allocation bitmap, and that is the design rather than an omission: with
|
||||
// files laid down contiguously, every block is either inside some entry's range or it is
|
||||
// not, so the directory already is the allocation map. A bitmap would be a second copy of
|
||||
// a fact that is already written down, and a second copy is a thing that can disagree.
|
||||
static long findFreeRun(const Directory *directory, const Superblock *super, uint16_t wanted) {
|
||||
if (wanted == 0) {
|
||||
// An empty file occupies nothing, so it has no start to speak of. Block 0 is the
|
||||
// superblock and can never hold file data, which makes it the honest way to say
|
||||
// "nowhere" without inventing a place that belongs to somebody else.
|
||||
(void)directory;
|
||||
return 0;
|
||||
}
|
||||
uint32_t firstData = (uint32_t)super->directoryStart + super->directoryBlocks;
|
||||
for (uint32_t candidate = firstData; candidate + wanted <= super->diskBlocks; candidate++) {
|
||||
uint32_t clash = 0;
|
||||
for (int i = 0; i < directory->entries && !clash; i++) {
|
||||
const uint8_t *entry = entryAt(directory, i);
|
||||
if (!entryInUse(entry)) {
|
||||
continue;
|
||||
}
|
||||
uint32_t start = readWord(entry + SBFS_ENTRY_START);
|
||||
uint32_t used = entryBlocksUsed(entry);
|
||||
if (used == 0) {
|
||||
continue;
|
||||
}
|
||||
if (candidate < start + used && start < candidate + wanted) {
|
||||
// Overlaps this file, so start looking again past the end of it.
|
||||
clash = start + used;
|
||||
}
|
||||
}
|
||||
if (clash) {
|
||||
candidate = clash - 1; // The loop's increment takes it to clash.
|
||||
continue;
|
||||
}
|
||||
return (long)candidate;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static uint16_t countFree(const Directory *directory, const Superblock *super) {
|
||||
uint32_t used = 0;
|
||||
for (int i = 0; i < directory->entries; i++) {
|
||||
const uint8_t *entry = entryAt(directory, i);
|
||||
if (entryInUse(entry)) {
|
||||
used += entryBlocksUsed(entry);
|
||||
}
|
||||
}
|
||||
// Block 0 and everything up to the end of the directory is not available.
|
||||
uint32_t overhead = (uint32_t)super->directoryStart + super->directoryBlocks;
|
||||
return (uint16_t)(super->diskBlocks - overhead - used);
|
||||
}
|
||||
|
||||
// ---- Commands ----
|
||||
|
||||
static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks) {
|
||||
if (blocks <= 1u + directoryBlocks) {
|
||||
fprintf(stderr, "Error: A disk of %u blocks has no room for a superblock and a"
|
||||
" directory of %u.\n", blocks, directoryBlocks);
|
||||
return 1;
|
||||
}
|
||||
// Quietly, because a disk that is not there yet is the ordinary case for format and
|
||||
// not something to complain about on the way past.
|
||||
FILE *image = fopen(path, "r+b");
|
||||
if (image == NULL) {
|
||||
image = openImage(path, "w+b");
|
||||
if (image == NULL) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
uint8_t empty[SBFS_BLOCK_BYTES];
|
||||
memset(empty, 0, sizeof(empty));
|
||||
for (uint16_t i = 0; i < blocks; i++) {
|
||||
if (writeBlock(image, i, empty)) {
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Superblock super;
|
||||
super.diskBlocks = blocks;
|
||||
super.directoryStart = SBFS_FIRST_DIRECTORY_BLOCK;
|
||||
super.directoryBlocks = directoryBlocks;
|
||||
super.freeBlocks = (uint16_t)(blocks - 1 - directoryBlocks);
|
||||
if (writeSuperblock(image, &super)) {
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
fclose(image);
|
||||
printf("Formatted %s: %u blocks, %u of directory, %u free.\n",
|
||||
path, blocks, directoryBlocks, super.freeBlocks);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int commandList(const char *path) {
|
||||
FILE *image = openImage(path, "rb");
|
||||
if (image == NULL) {
|
||||
return 1;
|
||||
}
|
||||
Superblock super;
|
||||
Directory directory;
|
||||
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
printf("%s: %u blocks, %u of directory, %u entries.\n",
|
||||
path, super.diskBlocks, super.directoryBlocks, directory.entries);
|
||||
printf("%-22s %8s %7s %7s\n", "NAME", "BYTES", "START", "BLOCKS");
|
||||
char name[SBFS_NAME_BYTES + 1];
|
||||
int shown = 0;
|
||||
for (int i = 0; i < directory.entries; i++) {
|
||||
const uint8_t *entry = entryAt(&directory, i);
|
||||
if (!entryInUse(entry)) {
|
||||
continue;
|
||||
}
|
||||
entryName(entry, name);
|
||||
printf("%-22s %8u %7u %7u\n", name, entrySize(entry),
|
||||
readWord(entry + SBFS_ENTRY_START), entryBlocksUsed(entry));
|
||||
shown++;
|
||||
}
|
||||
// The count in the superblock is a cache, so say what the directory actually adds up
|
||||
// to as well. If the two ever disagree, the directory is the one to believe.
|
||||
uint16_t counted = countFree(&directory, &super);
|
||||
printf("%d file%s, %u blocks free", shown, shown == 1 ? "" : "s", counted);
|
||||
if (counted != super.freeBlocks) {
|
||||
printf(" (the superblock says %u, which is stale)", super.freeBlocks);
|
||||
}
|
||||
printf(".\n");
|
||||
free(directory.bytes);
|
||||
fclose(image);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int commandPut(const char *path, const char *hostFile, const char *asName) {
|
||||
FILE *source = fopen(hostFile, "rb");
|
||||
if (source == NULL) {
|
||||
fprintf(stderr, "Error: Couldn't open \"%s\".\n", hostFile);
|
||||
return 1;
|
||||
}
|
||||
fseek(source, 0, SEEK_END);
|
||||
long size = ftell(source);
|
||||
rewind(source);
|
||||
if (size < 0) {
|
||||
fprintf(stderr, "Error: Couldn't measure \"%s\".\n", hostFile);
|
||||
fclose(source);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (strlen(asName) > SBFS_NAME_BYTES) {
|
||||
fprintf(stderr, "Error: \"%s\" is %zu characters, and a name may be %d.\n"
|
||||
" Give a shorter one as the last argument.\n",
|
||||
asName, strlen(asName), SBFS_NAME_BYTES);
|
||||
fclose(source);
|
||||
return 1;
|
||||
}
|
||||
|
||||
FILE *image = openImage(path, "r+b");
|
||||
if (image == NULL) {
|
||||
fclose(source);
|
||||
return 1;
|
||||
}
|
||||
Superblock super;
|
||||
Directory directory;
|
||||
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
|
||||
fclose(source);
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
if (findByName(&directory, asName) >= 0) {
|
||||
fprintf(stderr, "Error: \"%s\" is already on the disk. Delete it first.\n", asName);
|
||||
goto failed;
|
||||
}
|
||||
int slot = -1;
|
||||
for (int i = 0; i < directory.entries && slot < 0; i++) {
|
||||
if (!entryInUse(entryAt(&directory, i))) {
|
||||
slot = i;
|
||||
}
|
||||
}
|
||||
if (slot < 0) {
|
||||
fprintf(stderr, "Error: The directory is full: %d entries, all taken.\n", directory.entries);
|
||||
goto failed;
|
||||
}
|
||||
|
||||
uint16_t whole = (uint16_t)SBFS_WHOLE_BLOCKS(size);
|
||||
uint8_t tail = (uint8_t)SBFS_TAIL_BYTES(size);
|
||||
uint16_t needed = (uint16_t)SBFS_BLOCKS_USED(whole, tail);
|
||||
long start = findFreeRun(&directory, &super, needed);
|
||||
if (start < 0) {
|
||||
fprintf(stderr, "Error: No run of %u free blocks. There may be room on the disk"
|
||||
" without there being room in one piece.\n", needed);
|
||||
goto failed;
|
||||
}
|
||||
|
||||
uint8_t block[SBFS_BLOCK_BYTES];
|
||||
for (uint16_t i = 0; i < needed; i++) {
|
||||
memset(block, 0, sizeof(block));
|
||||
size_t got = fread(block, 1, SBFS_BLOCK_BYTES, source);
|
||||
if (got == 0 && i < needed) {
|
||||
fprintf(stderr, "Error: \"%s\" ended sooner than its size said.\n", hostFile);
|
||||
goto failed;
|
||||
}
|
||||
if (writeBlock(image, (uint16_t)(start + i), block)) {
|
||||
goto failed;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t *entry = entryAt(&directory, slot);
|
||||
memset(entry, 0, SBFS_ENTRY_BYTES);
|
||||
entry[SBFS_ENTRY_FLAGS] = SBFS_FLAG_IN_USE;
|
||||
writeWord(entry + SBFS_ENTRY_START, (uint16_t)start);
|
||||
writeWord(entry + SBFS_ENTRY_BLOCKS, whole);
|
||||
entry[SBFS_ENTRY_TAIL] = tail;
|
||||
memcpy(entry + SBFS_ENTRY_NAME, asName, strlen(asName));
|
||||
|
||||
super.freeBlocks = countFree(&directory, &super);
|
||||
if (writeDirectory(image, &super, &directory) || writeSuperblock(image, &super)) {
|
||||
goto failed;
|
||||
}
|
||||
printf("Put %s on as \"%s\": %ld bytes at block %ld.\n", hostFile, asName, size, start);
|
||||
free(directory.bytes);
|
||||
fclose(source);
|
||||
fclose(image);
|
||||
return 0;
|
||||
|
||||
failed:
|
||||
free(directory.bytes);
|
||||
fclose(source);
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int commandGet(const char *path, const char *name, const char *hostFile) {
|
||||
FILE *image = openImage(path, "rb");
|
||||
if (image == NULL) {
|
||||
return 1;
|
||||
}
|
||||
Superblock super;
|
||||
Directory directory;
|
||||
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
int slot = findByName(&directory, name);
|
||||
if (slot < 0) {
|
||||
fprintf(stderr, "Error: There is no \"%s\" on that disk.\n", name);
|
||||
free(directory.bytes);
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
const uint8_t *entry = entryAt(&directory, slot);
|
||||
uint32_t size = entrySize(entry);
|
||||
uint16_t start = readWord(entry + SBFS_ENTRY_START);
|
||||
uint16_t used = entryBlocksUsed(entry);
|
||||
|
||||
FILE *out = fopen(hostFile, "wb");
|
||||
if (out == NULL) {
|
||||
fprintf(stderr, "Error: Couldn't write \"%s\".\n", hostFile);
|
||||
free(directory.bytes);
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
uint8_t block[SBFS_BLOCK_BYTES];
|
||||
uint32_t left = size;
|
||||
for (uint16_t i = 0; i < used; i++) {
|
||||
if (readBlock(image, (uint16_t)(start + i), block)) {
|
||||
fclose(out);
|
||||
free(directory.bytes);
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
uint32_t take = (left < SBFS_BLOCK_BYTES) ? left : SBFS_BLOCK_BYTES;
|
||||
fwrite(block, 1, take, out);
|
||||
left -= take;
|
||||
}
|
||||
fclose(out);
|
||||
printf("Got \"%s\" off as %s: %u bytes.\n", name, hostFile, size);
|
||||
free(directory.bytes);
|
||||
fclose(image);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int commandDelete(const char *path, const char *name) {
|
||||
FILE *image = openImage(path, "r+b");
|
||||
if (image == NULL) {
|
||||
return 1;
|
||||
}
|
||||
Superblock super;
|
||||
Directory directory;
|
||||
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
int slot = findByName(&directory, name);
|
||||
if (slot < 0) {
|
||||
fprintf(stderr, "Error: There is no \"%s\" on that disk.\n", name);
|
||||
free(directory.bytes);
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
// A deleted entry and a never used one are the same thing: the in use bit goes down
|
||||
// and its blocks are free again. The blocks themselves are left as they were, which
|
||||
// is worth knowing if anything is ever meant to be private.
|
||||
memset(entryAt(&directory, slot), 0, SBFS_ENTRY_BYTES);
|
||||
super.freeBlocks = countFree(&directory, &super);
|
||||
int failed = writeDirectory(image, &super, &directory) || writeSuperblock(image, &super);
|
||||
if (!failed) {
|
||||
printf("Deleted \"%s\". %u blocks free.\n", name, super.freeBlocks);
|
||||
}
|
||||
free(directory.bytes);
|
||||
fclose(image);
|
||||
return failed;
|
||||
}
|
||||
|
||||
static void printUsage(const char *program) {
|
||||
printf("Usage: %s <command> <image> [arguments]\n", program);
|
||||
printf("\n");
|
||||
printf("Commands:\n");
|
||||
printf(" format <image> [blocks] [dirblocks] Lay down a fresh filesystem.\n");
|
||||
printf(" list <image> Show what is on the disk.\n");
|
||||
printf(" put <image> <file> [name] Put a host file onto it.\n");
|
||||
printf(" get <image> <name> [file] Take one off it.\n");
|
||||
printf(" delete <image> <name> Remove one.\n");
|
||||
printf("\n");
|
||||
printf("Blocks are %d bytes. A name may be %d characters. Without one, put uses the\n",
|
||||
SBFS_BLOCK_BYTES, SBFS_NAME_BYTES);
|
||||
printf("file's own name, which is often too long, and it will say so.\n");
|
||||
}
|
||||
|
||||
// The part of a path after the last separator, so that put can default to a file's own
|
||||
// name rather than the whole path it was found at.
|
||||
static const char *baseName(const char *path) {
|
||||
const char *slash = strrchr(path, '/');
|
||||
return slash ? slash + 1 : path;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
if (argc < 2 || strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) {
|
||||
printUsage(argv[0]);
|
||||
return argc < 2 ? 1 : 0;
|
||||
}
|
||||
const char *command = argv[1];
|
||||
if (argc < 3) {
|
||||
fprintf(stderr, "Error: %s needs a disk image.\n", command);
|
||||
return 1;
|
||||
}
|
||||
const char *path = argv[2];
|
||||
|
||||
if (strcmp(command, "format") == 0) {
|
||||
long blocks = (argc > 3) ? strtol(argv[3], NULL, 0) : 512;
|
||||
long directoryBlocks = (argc > 4) ? strtol(argv[4], NULL, 0) : SBFS_DEFAULT_DIRECTORY_BLOCKS;
|
||||
if (blocks < 2 || blocks > 0xFFFF || directoryBlocks < 1 || directoryBlocks > 0xFFFF) {
|
||||
fprintf(stderr, "Error: A disk is between 2 and 65535 blocks, with at least"
|
||||
" one of directory.\n");
|
||||
return 1;
|
||||
}
|
||||
return commandFormat(path, (uint16_t)blocks, (uint16_t)directoryBlocks);
|
||||
}
|
||||
if (strcmp(command, "list") == 0) {
|
||||
return commandList(path);
|
||||
}
|
||||
if (strcmp(command, "put") == 0) {
|
||||
if (argc < 4) {
|
||||
fprintf(stderr, "Error: put needs a file to put on.\n");
|
||||
return 1;
|
||||
}
|
||||
return commandPut(path, argv[3], (argc > 4) ? argv[4] : baseName(argv[3]));
|
||||
}
|
||||
if (strcmp(command, "get") == 0) {
|
||||
if (argc < 4) {
|
||||
fprintf(stderr, "Error: get needs the name of a file on the disk.\n");
|
||||
return 1;
|
||||
}
|
||||
return commandGet(path, argv[3], (argc > 4) ? argv[4] : argv[3]);
|
||||
}
|
||||
if (strcmp(command, "delete") == 0) {
|
||||
if (argc < 4) {
|
||||
fprintf(stderr, "Error: delete needs the name of a file on the disk.\n");
|
||||
return 1;
|
||||
}
|
||||
return commandDelete(path, argv[3]);
|
||||
}
|
||||
fprintf(stderr, "Error: There is no \"%s\" command.\n", command);
|
||||
printUsage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// sbex.h
|
||||
// The SplitBit loadable program format, version one.
|
||||
//
|
||||
// A program that is not the one the machine booted from has to say where it wants to
|
||||
// live, because nothing relocates it. This is a header saying that, in front of the
|
||||
// bytes themselves. It is the same idea as the load address on the front of a C64 .PRG,
|
||||
// with room for the machine to ask a few more questions later.
|
||||
//
|
||||
// Two things read this: whatever builds one on the host, and the loader running on
|
||||
// SplitBit. As with the filesystem, nothing is shared between them but the specification.
|
||||
//
|
||||
// All multi byte numbers are most significant byte first.
|
||||
//
|
||||
// 0 4 "SBEX"
|
||||
// 4 1 Version
|
||||
// 5 1 Reserved
|
||||
// 6 2 Where the code goes in Program Memory
|
||||
// 8 2 Where to start running, an address in Program Memory
|
||||
// 10 2 How many bytes of code there are
|
||||
// 12 2 Where the data goes in Data Memory
|
||||
// 14 2 How many bytes of data there are
|
||||
// 16 The code, then the data
|
||||
//
|
||||
// Sixteen bytes, so the code begins at a round offset and finding it is one step rather
|
||||
// than an arithmetic. Nothing here relocates anything: the addresses are where the
|
||||
// program was built to live, and putting it anywhere else would leave every branch and
|
||||
// every SETD inside it pointing at the wrong place.
|
||||
//
|
||||
// Written by Anachronaut
|
||||
|
||||
#ifndef SBEX_H
|
||||
#define SBEX_H
|
||||
|
||||
#define SBEX_MAGIC "SBEX"
|
||||
#define SBEX_MAGIC_BYTES 4
|
||||
#define SBEX_VERSION 1
|
||||
#define SBEX_HEADER_BYTES 16
|
||||
|
||||
#define SBEX_VERSION_AT 4
|
||||
#define SBEX_CODE_AT 6
|
||||
#define SBEX_ENTRY_AT 8
|
||||
#define SBEX_CODE_LEN_AT 10
|
||||
#define SBEX_DATA_AT 12
|
||||
#define SBEX_DATA_LEN_AT 14
|
||||
|
||||
#endif // SBEX_H
|
||||
@@ -0,0 +1,79 @@
|
||||
// sbfs.h
|
||||
// The SplitBit Filesystem, version one.
|
||||
//
|
||||
// This is the host side's copy of the format. The other implementation is SplitBit
|
||||
// assembly running on the machine itself, so nothing can be shared between them except
|
||||
// the specification: the two have to be kept honest by a document rather than by a
|
||||
// header. Everything here follows that document exactly, and anything that changes here
|
||||
// has to change there in the same breath.
|
||||
//
|
||||
// All multi byte numbers are most significant byte first, the same as every other number
|
||||
// SplitBit stores: addresses, the SPBT binary header, and the vector table.
|
||||
//
|
||||
// Written by Anachronaut
|
||||
|
||||
#ifndef SBFS_H
|
||||
#define SBFS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define SBFS_MAGIC "SBFS"
|
||||
#define SBFS_MAGIC_BYTES 4
|
||||
#define SBFS_VERSION 1
|
||||
|
||||
#define SBFS_BLOCK_BYTES 256
|
||||
|
||||
// ---- Block 0, the superblock ----
|
||||
//
|
||||
// 0 4 "SBFS"
|
||||
// 4 1 Version
|
||||
// 5 1 Reserved
|
||||
// 6 2 Blocks on the disk
|
||||
// 8 2 First directory block
|
||||
// 10 2 Blocks the directory occupies
|
||||
// 12 2 Free blocks, a cache rather than the authority
|
||||
// 14 Reserved to the end of the block
|
||||
|
||||
#define SBFS_SUPER_VERSION 4
|
||||
#define SBFS_SUPER_DISK 6
|
||||
#define SBFS_SUPER_DIRSTART 8
|
||||
#define SBFS_SUPER_DIRBLOCKS 10
|
||||
#define SBFS_SUPER_FREE 12
|
||||
|
||||
// ---- Directory entries ----
|
||||
//
|
||||
// 0 1 Flags
|
||||
// 1 2 First block of the file's data
|
||||
// 3 2 Whole blocks the file occupies
|
||||
// 5 1 Bytes in the trailing part block, or zero if there is not one
|
||||
// 6 22 Name, padded with zeroes
|
||||
// 28 4 Reserved
|
||||
//
|
||||
// Thirty two divides two hundred and fifty six, so an entry never straddles a block and
|
||||
// reading one never means handling a split.
|
||||
|
||||
#define SBFS_ENTRY_BYTES 32
|
||||
#define SBFS_ENTRIES_PER_BLOCK (SBFS_BLOCK_BYTES / SBFS_ENTRY_BYTES)
|
||||
|
||||
#define SBFS_ENTRY_FLAGS 0
|
||||
#define SBFS_ENTRY_START 1
|
||||
#define SBFS_ENTRY_BLOCKS 3
|
||||
#define SBFS_ENTRY_TAIL 5
|
||||
#define SBFS_ENTRY_NAME 6
|
||||
#define SBFS_NAME_BYTES 22
|
||||
|
||||
#define SBFS_FLAG_IN_USE 0x01
|
||||
|
||||
// The directory begins at block 1 and is this many blocks unless told otherwise, which
|
||||
// is sixty four files. The superblock carries the real number, so this is only what a
|
||||
// freshly formatted disk gets.
|
||||
#define SBFS_FIRST_DIRECTORY_BLOCK 1
|
||||
#define SBFS_DEFAULT_DIRECTORY_BLOCKS 8
|
||||
|
||||
// A file of n bytes occupies n / 256 whole blocks and, if anything is left over, one more
|
||||
// for the tail. Zero means zero in both, so an empty file occupies nothing at all.
|
||||
#define SBFS_WHOLE_BLOCKS(bytes) ((bytes) / SBFS_BLOCK_BYTES)
|
||||
#define SBFS_TAIL_BYTES(bytes) ((bytes) % SBFS_BLOCK_BYTES)
|
||||
#define SBFS_BLOCKS_USED(blocks, tail) ((blocks) + ((tail) ? 1 : 0))
|
||||
|
||||
#endif // SBFS_H
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Wraps an assembled SplitBit binary into a loadable program.
|
||||
|
||||
The assembler emits a boot image: a Program Segment that loads at zero and a Data Segment
|
||||
that does the same. A program meant to be loaded somewhere else has to say where it goes,
|
||||
which is what the SBEX header in front of it is for.
|
||||
|
||||
A program says where it lives by reserving the front of each segment, so the addresses
|
||||
given here have to match the reserves in its source. Nothing checks that for you, and
|
||||
nothing relocates anything if you get it wrong.
|
||||
"""
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
def segments(raw):
|
||||
at = 4 + 1 + 4 # magic, version, feature flags
|
||||
assert raw[:4] == b"SPBT", "not a SplitBit binary"
|
||||
assert raw[at:at + 3] == b"PRG"
|
||||
plen = struct.unpack(">H", raw[at + 3:at + 5])[0]
|
||||
program = raw[at + 5:at + 5 + plen]
|
||||
at = at + 5 + plen
|
||||
assert raw[at:at + 3] == b"DAT"
|
||||
dlen = struct.unpack(">H", raw[at + 3:at + 5])[0]
|
||||
return program, raw[at + 5:at + 5 + dlen]
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 6:
|
||||
sys.exit("usage: wrap.py <binary> <output> <code address> <data address> <entry>")
|
||||
binary, output = sys.argv[1], sys.argv[2]
|
||||
codeAt, dataAt, entry = (int(a, 0) for a in sys.argv[3:6])
|
||||
|
||||
program, data = segments(open(binary, "rb").read())
|
||||
|
||||
# Everything below the address a segment is placed at is the padding the reserve put
|
||||
# there, and is not part of the program.
|
||||
code = program[codeAt:]
|
||||
values = data[dataAt:]
|
||||
|
||||
header = bytearray(16)
|
||||
header[0:4] = b"SBEX"
|
||||
header[4] = 1
|
||||
struct.pack_into(">H", header, 6, codeAt)
|
||||
struct.pack_into(">H", header, 8, entry)
|
||||
struct.pack_into(">H", header, 10, len(code))
|
||||
struct.pack_into(">H", header, 12, dataAt)
|
||||
struct.pack_into(">H", header, 14, len(values))
|
||||
|
||||
with open(output, "wb") as out:
|
||||
out.write(header)
|
||||
out.write(code)
|
||||
out.write(values)
|
||||
print("%s: %d bytes of code at 0x%04X, %d of data at 0x%04X, entry 0x%04X"
|
||||
% (output, len(code), codeAt, len(values), dataAt, entry))
|
||||
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user