// 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 #include #include // For mirroring a host directory onto a disk: walking one, and telling a directory from a // file. POSIX rather than C, which is why the build asks for POSIX.1-2008 by name. #include #include // 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 { uint8_t version; uint16_t diskBlocks; uint16_t directoryStart; uint16_t directoryBlocks; uint16_t freeBlocks; uint16_t bootBlocks; // Per slot. Zero on a disk that cannot be booted. uint8_t bootSlot; // Which of the two is live. uint8_t bootState; // How the last start went. See sbfs.h. } 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; } // Either version is read. A version one disk has zeroes in the bytes version two // uses for a parent, and zero is the root, so every file on it reads as a file in the // root - which is exactly where it is. Nothing is converted here. if (block[SBFS_SUPER_VERSION] != SBFS_VERSION_FLAT && block[SBFS_SUPER_VERSION] != SBFS_VERSION_TREE) { fprintf(stderr, "Error: That disk is version %u, and this understands %u and %u.\n", block[SBFS_SUPER_VERSION], SBFS_VERSION_FLAT, SBFS_VERSION_TREE); return 1; } // A directory big enough that its last entries cannot be named as a parent. See // sbfs.h: those entries do not refuse what is put in them, they quietly put it in the // root instead. Refused on the way in, so that nothing below ever has to wonder. uint16_t directoryBlocks = readWord(block + SBFS_SUPER_DIRBLOCKS); if (directoryBlocks > SBFS_MAX_DIRECTORY_BLOCKS) { fprintf(stderr, "Error: That disk claims %u directory blocks, and %u is the most" " that leaves every entry able to be named as a parent.\n", directoryBlocks, SBFS_MAX_DIRECTORY_BLOCKS); return 1; } super->version = block[SBFS_SUPER_VERSION]; super->diskBlocks = readWord(block + SBFS_SUPER_DISK); super->directoryStart = readWord(block + SBFS_SUPER_DIRSTART); super->directoryBlocks = directoryBlocks; super->freeBlocks = readWord(block + SBFS_SUPER_FREE); super->bootBlocks = readWord(block + SBFS_SUPER_BOOTBLOCKS); super->bootSlot = block[SBFS_SUPER_BOOTSLOT]; super->bootState = block[SBFS_SUPER_BOOTSTATE]; // The boot area and the directory's position describe the same fact from two sides, // so they have to agree or one of them is wrong and there is no way to tell which. uint32_t expected = SBFS_FIRST_BOOT_BLOCK + (uint32_t)super->bootBlocks * SBFS_BOOT_SLOTS; if (super->directoryStart != expected) { fprintf(stderr, "Error: That disk says %u blocks of boot area and puts its" " directory at %u, which should then be %u.\n", super->bootBlocks, super->directoryStart, expected); return 1; } if (super->bootSlot >= SBFS_BOOT_SLOTS) { fprintf(stderr, "Error: That disk names boot slot %u, and there are %u.\n", super->bootSlot, SBFS_BOOT_SLOTS); return 1; } 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); // Written back at the version it was read at. Putting a file on a flat disk leaves it // flat; only creating a directory on it makes the difference real, and only mkdir // raises the number. block[SBFS_SUPER_VERSION] = super->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); writeWord(block + SBFS_SUPER_BOOTBLOCKS, super->bootBlocks); block[SBFS_SUPER_BOOTSLOT] = super->bootSlot; block[SBFS_SUPER_BOOTSTATE] = super->bootState; 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 int entryIsDirectory(const uint8_t *entry) { return (entry[SBFS_ENTRY_FLAGS] & SBFS_FLAG_DIRECTORY) != 0; } // A save that was interrupted between writing its temporary and committing it. The blocks // are genuinely spoken for - entryInUse says so, and the allocator must keep believing it // - but nothing has claimed them under a name anybody asked for. static int entryIsTemporary(const uint8_t *entry) { return (entry[SBFS_ENTRY_FLAGS] & SBFS_FLAG_TEMPORARY) != 0; } // The index of the entry this one lives in, or -1 for the root. Stored as index plus one // so that a zeroed field - which is what every version one entry has - means the root. static int entryParent(const uint8_t *entry) { return SBFS_PARENT_INDEX(readWord(entry + SBFS_ENTRY_PARENT)); } static void entrySetParent(uint8_t *entry, int parent) { writeWord(entry + SBFS_ENTRY_PARENT, parent < 0 ? SBFS_PARENT_ROOT : SBFS_PARENT_OF(parent)); } 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'; } // ---- Paths ---- // // A path is names separated by '/', and every path this tool is given is from the root: // there is no working directory on the host, and one would have nowhere to live between // two runs of a command line tool. The machine is the side that gets one of those. // // The root is not an entry. It is the absence of a parent, so -1 stands for it throughout // and is a perfectly good answer rather than a failure - which is why every function here // returns its outcome separately from the index it found. // How long a path this tool will carry. Nothing in the format says: a path is not stored // anywhere, it is only ever walked, and what is stored is one name and one parent. #define SBFS_PATH_BYTES 512 // Copies the next name out of a path and returns where the path goes on, or NULL when // there are no more. Empty pieces - a leading separator, a doubled one, a trailing one - // are skipped rather than refused, so "/Apps/" and "Apps" walk the same way. static const char *nextComponent(const char *path, char *into, int *tooLong) { while (*path == SBFS_SEPARATOR) { path++; } if (*path == '\0') { return NULL; } size_t n = 0; while (*path != '\0' && *path != SBFS_SEPARATOR) { if (n < SBFS_NAME_BYTES) { into[n] = *path; } n++; path++; } *tooLong = (n > SBFS_NAME_BYTES); into[n > SBFS_NAME_BYTES ? SBFS_NAME_BYTES : n] = '\0'; return path; } // One named child of one directory. `parent` is an entry index, or -1 for the root. // // This is the whole of what a directory is. There is no list of children anywhere: being // a child is a fact written in the child, so finding them means looking at all of them. // That is the same walk the flat version did, with one more thing compared. static int findIn(const Directory *directory, const char *name, int parent) { 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; } if (entryParent(entry) != parent) { continue; } entryName(entry, held); if (strcmp(held, name) == 0) { return i; } } return -1; } // Walks a whole path. Zero if it got there, and then *index is the entry it names or -1 // for the root. Anything else leaves *why saying what stopped it. static int resolve(const Directory *directory, const char *path, int *index, const char **why) { int at = -1; char name[SBFS_NAME_BYTES + 1]; int tooLong = 0; const char *rest = path; while ((rest = nextComponent(rest, name, &tooLong)) != NULL) { if (tooLong) { *why = "has a name longer than 22 characters in it"; return 1; } if (strcmp(name, ".") == 0) { continue; } if (strcmp(name, "..") == 0) { if (at >= 0) { at = entryParent(entryAt(directory, at)); } continue; // ".." from the root is the root. } // Only a directory can be walked through. The LAST thing on the path is not // checked here, because only the caller knows whether it wanted a file or a // directory, and it can say so far better than this can. if (at >= 0 && !entryIsDirectory(entryAt(directory, at))) { *why = "has something in it that is not a directory"; return 1; } int found = findIn(directory, name, at); if (found < 0) { *why = "is not there"; return 1; } at = found; } *index = at; return 0; } // Walks all but the last name, so that a caller can make the last one. *parent comes back // as the directory to make it in, and `leaf` as the name to make. `leaf` must have room // for SBFS_NAME_BYTES + 1. static int resolveParent(const Directory *directory, const char *path, char *leaf, int *parent, const char **why) { const char *cut = strrchr(path, SBFS_SEPARATOR); const char *last = cut ? cut + 1 : path; if (*last == '\0') { *why = "ends in a separator, so it does not name anything"; return 1; } if (strlen(last) > SBFS_NAME_BYTES) { *why = "ends in a name longer than 22 characters"; return 1; } if (strcmp(last, ".") == 0 || strcmp(last, "..") == 0) { *why = "ends in a name that is already taken by the filesystem"; return 1; } strcpy(leaf, last); if (cut == NULL) { *parent = -1; // A bare name belongs in the root. return 0; } char head[SBFS_PATH_BYTES]; size_t n = (size_t)(cut - path); if (n >= sizeof(head)) { *why = "is longer than this tool will carry"; return 1; } memcpy(head, path, n); head[n] = '\0'; return resolve(directory, head, parent, why); // "" is the root, which is correct. } // Whether something can be made inside *parent at all. The root always can. static int parentIsUsable(const Directory *directory, int parent, const char *path) { if (parent >= 0 && !entryIsDirectory(entryAt(directory, parent))) { fprintf(stderr, "Error: \"%s\" is inside something that is not a directory.\n", path); return 0; } return 1; } // Whether a directory has anything in it. Nothing points down, so this is the only way // to know: something is in it if something says it is. static int directoryHasChildren(const Directory *directory, int parent) { for (int i = 0; i < directory->entries; i++) { const uint8_t *entry = entryAt(directory, i); if (entryInUse(entry) && entryParent(entry) == parent) { return 1; } } return 0; } // 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, uint16_t bootBlocks) { if (directoryBlocks > SBFS_MAX_DIRECTORY_BLOCKS) { fprintf(stderr, "Error: %u directory blocks is %u entries, and entry 65535 has no" " parent number - adding one wraps to zero, which is the root." " %u blocks is the most, giving %u entries.\n", directoryBlocks, (unsigned)directoryBlocks * SBFS_ENTRIES_PER_BLOCK, SBFS_MAX_DIRECTORY_BLOCKS, SBFS_MAX_ENTRIES); return 1; } uint32_t overhead = 1u + (uint32_t)bootBlocks * SBFS_BOOT_SLOTS + directoryBlocks; if (blocks <= overhead) { fprintf(stderr, "Error: A disk of %u blocks has no room for a superblock, %u of" " boot area and a directory of %u.\n", blocks, (unsigned)((uint32_t)bootBlocks * SBFS_BOOT_SLOTS), 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; // A DISK IS AT THE LOWEST VERSION THAT DESCRIBES WHAT IS ON IT, and a fresh one has // no directories on it, so it is flat and flat is version one. The number says what // the disk contains, not which tool made it - which is what lets everything built // here still be mounted by a reader that has never heard of a directory. mkdir is // what raises it, because mkdir is what makes the difference true. super.version = SBFS_VERSION_FLAT; super.diskBlocks = blocks; // THE DIRECTORY MOVES UP BY THE BOOT AREA, and that is the whole mechanism. Both // implementations already work out the first usable block as directoryStart plus // directoryBlocks, so everything below the directory is reserved by arithmetic that // was there before any of this, and no allocator changed. super.directoryStart = (uint16_t)(SBFS_FIRST_BOOT_BLOCK + (uint32_t)bootBlocks * SBFS_BOOT_SLOTS); super.directoryBlocks = directoryBlocks; super.freeBlocks = (uint16_t)(blocks - overhead); super.bootBlocks = bootBlocks; super.bootSlot = 0; super.bootState = SBFS_BOOT_SETTLED; if (writeSuperblock(image, &super)) { fclose(image); return 1; } fclose(image); if (bootBlocks) { printf("Formatted %s: %u blocks, two boot slots of %u, %u of directory, %u free.\n", path, blocks, bootBlocks, directoryBlocks, super.freeBlocks); return 0; } printf("Formatted %s: %u blocks, %u of directory, %u free.\n", path, blocks, directoryBlocks, super.freeBlocks); return 0; } // The full path of an entry, built by walking up the parents and writing them out // backwards. Nothing stores a path, so this is the only way to have one. // // The depth cap is what makes this safe on a disk whose parents form a loop: walking up // would otherwise never reach the root. Running out of depth is reported as not fitting, // which is what it is. static int entryPath(const Directory *directory, int index, char *into, size_t room) { int chain[64]; int depth = 0; while (index >= 0 && depth < (int)(sizeof(chain) / sizeof(chain[0]))) { chain[depth++] = index; index = entryParent(entryAt(directory, index)); } if (index >= 0) { return -1; // Deeper than this will walk, or a loop. } size_t at = 0; char name[SBFS_NAME_BYTES + 1]; for (int i = depth - 1; i >= 0; i--) { entryName(entryAt(directory, chain[i]), name); size_t n = strlen(name); if (at + n + 2 > room) { return -1; } into[at++] = SBFS_SEPARATOR; memcpy(into + at, name, n); at += n; } into[at] = '\0'; return (int)at; } // What a listing added up to. Kept together because the two numbers are only meaningful // beside each other: a disk runs out of entries, not of files. typedef struct { int files; int directories; int temporaries; } Tally; // Prints one directory and everything under it, depth first and in entry order, which is // the order the machine will walk them in too. // // Recursing from the root can never loop, because it only ever descends into entries // whose parent is where it already is. A cycle among entries is therefore not an infinite // walk - it is a set of entries this never reaches, which is what the caller counts. static void listTree(const Directory *directory, int parent, char *prefix, size_t at, Tally *tally) { char name[SBFS_NAME_BYTES + 1]; for (int i = 0; i < directory->entries; i++) { const uint8_t *entry = entryAt(directory, i); if (!entryInUse(entry) || entryParent(entry) != parent) { continue; } entryName(entry, name); size_t n = strlen(name); if (at + n + 2 >= SBFS_PATH_BYTES) { continue; // Deeper than this tool will print. } prefix[at] = SBFS_SEPARATOR; memcpy(prefix + at + 1, name, n + 1); if (entryIsDirectory(entry)) { printf("%-34s %8s %7s %7s\n", prefix, "dir", "-", "-"); tally->directories++; listTree(directory, i, prefix, at + 1 + n, tally); } else { printf("%-34s %8u %7u %7u%s\n", prefix, entrySize(entry), readWord(entry + SBFS_ENTRY_START), entryBlocksUsed(entry), entryIsTemporary(entry) ? " unfinished" : ""); tally->files++; tally->temporaries += entryIsTemporary(entry); } prefix[at] = '\0'; } } static int commandList(const char *path, const char *within) { 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, version %u.\n", path, super.diskBlocks, super.directoryBlocks, directory.entries, super.version); printf("%-34s %8s %7s %7s\n", "NAME", "BYTES", "START", "BLOCKS"); char prefix[SBFS_PATH_BYTES]; Tally tally = { 0, 0, 0 }; int start = -1; if (within != NULL) { const char *why = NULL; if (resolve(&directory, within, &start, &why)) { fprintf(stderr, "Error: \"%s\" %s.\n", within, why); free(directory.bytes); fclose(image); return 1; } if (start >= 0 && !entryIsDirectory(entryAt(&directory, start))) { fprintf(stderr, "Error: \"%s\" is not a directory.\n", within); free(directory.bytes); fclose(image); return 1; } } // Listing one directory still prints whole paths, because a path that is only true // relative to an argument the reader cannot see is worse than no path at all. size_t from = 0; prefix[0] = '\0'; if (start >= 0) { int n = entryPath(&directory, start, prefix, sizeof(prefix)); if (n < 0) { fprintf(stderr, "Error: \"%s\" is nested too deep to print.\n", within); free(directory.bytes); fclose(image); return 1; } from = (size_t)n; } listTree(&directory, start, prefix, from, &tally); // 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", tally.files, tally.files == 1 ? "" : "s"); if (tally.directories > 0) { printf(", %d director%s", tally.directories, tally.directories == 1 ? "y" : "ies"); } // Entries are the ceiling nobody notices until they hit it, and on a disk of small // files they run out long before the blocks do. Saying both means never having to // work out which one is about to bite. printf(", %d of %d entries used, %u blocks free", tally.files + tally.directories, directory.entries, counted); if (counted != super.freeBlocks) { printf(" (the superblock says %u, which is stale)", super.freeBlocks); } printf(".\n"); // A save that stopped between deleting the old entry and naming the new one. The // bytes are all there under the temporary's name and one rename brings them back, // which is the whole of the recovery this format offers - so the thing that matters // is that a listing says so rather than showing a file with an odd name. if (tally.temporaries > 0) { printf("%d unfinished write%s: the blocks are held and the data is there, under" " that name. Rename it to keep it, delete it to let the blocks go.\n", tally.temporaries, tally.temporaries == 1 ? "" : "s"); } // Everything in use should have been reached by walking down from the root. Anything // that was not is pointing at a parent that is not there, or at itself, and that is // worth saying out loud rather than quietly leaving off the listing. if (within == NULL) { int inUse = 0; for (int i = 0; i < directory.entries; i++) { if (entryInUse(entryAt(&directory, i))) { inUse++; } } if (inUse != tally.files + tally.directories) { printf("%d entr%s in use but not reachable from the root.\n", inUse - (tally.files + tally.directories), inUse - (tally.files + tally.directories) == 1 ? "y is" : "ies are"); } } free(directory.bytes); fclose(image); return 0; } // ---- Writing a boot slot ---- // // Raw blocks, outside the filesystem, with no entry and no name. That is what makes this // different from put: there is nothing to rename, so the safety comes from writing the // slot that is NOT live and moving one byte afterwards. // // The one byte is moved by a separate command on purpose. Writing a slot and choosing it // are different decisions - a slot can be written now and chosen after it has been looked // at - and putting them in one command would make every write a commitment. static int commandBoot(const char *path, const char *hostFile, long slot) { FILE *image = openImage(path, "r+b"); if (image == NULL) { return 1; } Superblock super; if (readSuperblock(image, &super)) { fclose(image); return 1; } if (super.bootBlocks == 0) { fprintf(stderr, "Error: That disk has no boot area. Format it with one.\n"); fclose(image); return 1; } if (slot < 0 || slot >= SBFS_BOOT_SLOTS) { fprintf(stderr, "Error: There are %d boot slots, numbered 0 and %d.\n", SBFS_BOOT_SLOTS, SBFS_BOOT_SLOTS - 1); fclose(image); return 1; } FILE *source = fopen(hostFile, "rb"); if (source == NULL) { fprintf(stderr, "Error: Couldn't open \"%s\".\n", hostFile); fclose(image); return 1; } if (fseek(source, 0, SEEK_END) != 0) { fprintf(stderr, "Error: Couldn't measure \"%s\".\n", hostFile); fclose(source); fclose(image); return 1; } long size = ftell(source); rewind(source); long blocks = (size + SBFS_BLOCK_BYTES - 1) / SBFS_BLOCK_BYTES; if (blocks > super.bootBlocks) { fprintf(stderr, "Error: \"%s\" is %ld bytes, which is %ld blocks, and a slot" " holds %u.\n", hostFile, size, blocks, super.bootBlocks); fclose(source); fclose(image); return 1; } // THE WHOLE SLOT IS WRITTEN, not just the part the file fills. A slot holding the tail // of whatever was there before is a slot whose contents depend on its history, and the // first stage reads all of it without knowing where the file stopped. uint16_t first = (uint16_t)(SBFS_FIRST_BOOT_BLOCK + (uint32_t)slot * super.bootBlocks); uint8_t block[SBFS_BLOCK_BYTES]; for (uint16_t i = 0; i < super.bootBlocks; i++) { memset(block, 0, sizeof(block)); size_t got = fread(block, 1, SBFS_BLOCK_BYTES, source); if (got == 0 && ferror(source)) { fprintf(stderr, "Error: Couldn't read \"%s\".\n", hostFile); fclose(source); fclose(image); return 1; } if (writeBlock(image, (uint16_t)(first + i), block)) { fclose(source); fclose(image); return 1; } } fclose(source); fclose(image); printf("Wrote %s into boot slot %ld: %ld bytes in %u blocks from block %u.%s\n", hostFile, slot, size, super.bootBlocks, first, slot == super.bootSlot ? "" : " It is not the live slot."); return 0; } // Choosing which slot the machine starts from. One byte, written on its own, so that the // change from one system to another is a single block write that either happened or did // not. static int commandBootSlot(const char *path, long slot) { FILE *image = openImage(path, "r+b"); if (image == NULL) { return 1; } Superblock super; if (readSuperblock(image, &super)) { fclose(image); return 1; } if (super.bootBlocks == 0) { fprintf(stderr, "Error: That disk has no boot area.\n"); fclose(image); return 1; } if (slot < 0 || slot >= SBFS_BOOT_SLOTS) { fprintf(stderr, "Error: There are %d boot slots, numbered 0 and %d.\n", SBFS_BOOT_SLOTS, SBFS_BOOT_SLOTS - 1); fclose(image); return 1; } super.bootSlot = (uint8_t)slot; if (writeSuperblock(image, &super)) { fclose(image); return 1; } fclose(image); printf("The machine now starts from boot slot %ld.\n", slot); return 0; } // ---- How the last start went, from the host ---- // // Shown with no argument and set with one. Setting it is how a disk that fell back is told // to try again, which is a decision rather than a repair: the thing that did not start has // to be fixed first, or the next start marks it and falls back once more. static const char *bootStateName(uint8_t state) { switch (state) { case SBFS_BOOT_SETTLED: return "settled, so the next start will use the configuration"; case SBFS_BOOT_TRYING: return "trying, so the last start never arrived"; case SBFS_BOOT_FELLBACK: return "fell back, and will keep doing so until settled"; default: return "a number this does not recognise"; } } static int commandBootState(const char *path, const char *setting) { FILE *image = openImage(path, setting ? "r+b" : "rb"); if (image == NULL) { return 1; } Superblock super; if (readSuperblock(image, &super)) { fclose(image); return 1; } if (setting == NULL) { printf("%u: %s\n", super.bootState, bootStateName(super.bootState)); fclose(image); return 0; } long wanted = strtol(setting, NULL, 0); if (wanted < 0 || wanted > SBFS_BOOT_FELLBACK) { fprintf(stderr, "Error: The boot state is 0, 1 or 2.\n"); fclose(image); return 1; } super.bootState = (uint8_t)wanted; if (writeSuperblock(image, &super)) { fclose(image); return 1; } fclose(image); printf("%ld: %s\n", wanted, bootStateName((uint8_t)wanted)); 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; } 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; } // Where it goes and what it is called are one argument. A bare name still means the // root, so every command line that worked before this still works. char leaf[SBFS_NAME_BYTES + 1]; int parent = -1; const char *why = NULL; if (resolveParent(&directory, asName, leaf, &parent, &why)) { fprintf(stderr, "Error: \"%s\" %s.\n", asName, why); goto failed; } if (!parentIsUsable(&directory, parent, asName)) { goto failed; } if (findIn(&directory, leaf, parent) >= 0) { fprintf(stderr, "Error: \"%s\" is already there. 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, leaf, strlen(leaf)); entrySetParent(entry, parent); 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 = -1; const char *why = NULL; if (resolve(&directory, name, &slot, &why) || slot < 0) { fprintf(stderr, "Error: \"%s\" %s.\n", name, why ? why : "is the root, which is not a file"); free(directory.bytes); fclose(image); return 1; } if (entryIsDirectory(entryAt(&directory, slot))) { fprintf(stderr, "Error: \"%s\" is a directory. There is nothing to take off.\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 = -1; const char *why = NULL; if (resolve(&directory, name, &slot, &why) || slot < 0) { fprintf(stderr, "Error: \"%s\" %s.\n", name, why ? why : "is the root, which cannot be deleted"); free(directory.bytes); fclose(image); return 1; } // delete is for files and rmdir is for directories, so that neither can be the one // that took away more than was asked for. if (entryIsDirectory(entryAt(&directory, slot))) { fprintf(stderr, "Error: \"%s\" is a directory. Use rmdir.\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; } // ---- mkdir ---- // // A directory costs one entry and no blocks at all. Its start, blocks and tail stay zero, // which is what keeps it out of the allocator's way: with files laid down contiguously an // entry with no range cannot overlap anything. // // This is also the only thing that raises a disk from version one to version two, because // it is the only thing that makes the difference between them real. static int commandMakeDirectory(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; } char leaf[SBFS_NAME_BYTES + 1]; int parent = -1; const char *why = NULL; int failed = 1; if (resolveParent(&directory, name, leaf, &parent, &why)) { fprintf(stderr, "Error: \"%s\" %s.\n", name, why); goto done; } if (!parentIsUsable(&directory, parent, name)) { goto done; } if (findIn(&directory, leaf, parent) >= 0) { fprintf(stderr, "Error: \"%s\" is already there.\n", name); goto done; } 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 done; } uint8_t *entry = entryAt(&directory, slot); memset(entry, 0, SBFS_ENTRY_BYTES); entry[SBFS_ENTRY_FLAGS] = SBFS_FLAG_IN_USE | SBFS_FLAG_DIRECTORY; memcpy(entry + SBFS_ENTRY_NAME, leaf, strlen(leaf)); entrySetParent(entry, parent); int raised = (super.version < SBFS_VERSION_TREE); super.version = SBFS_VERSION_TREE; super.freeBlocks = countFree(&directory, &super); if (writeDirectory(image, &super, &directory) || writeSuperblock(image, &super)) { goto done; } printf("Made \"%s\" as entry %d.\n", name, slot); if (raised) { printf("The disk is now version %d, because it has a directory on it.\n", SBFS_VERSION_TREE); } failed = 0; done: free(directory.bytes); fclose(image); return failed; } // ---- rmdir ---- // // Refuses a directory with anything in it, and that refusal is not politeness. Parents // are entry indices, and a freed index is handed out again to the next thing put on the // disk - so the children of a deleted directory would reappear inside whatever took its // place. Emptying it first is the only safe order, and making the caller do that is the // smallest way to guarantee it. // ---- Mirroring a host directory onto a disk ---- // // So that putting a new program where the others live is all it takes to have it on the // machine. A list of files in a makefile is a list that goes stale the moment somebody adds // something and forgets, and the thing they forgot is invisible until they look for it. // // EVERY FILE GOES THROUGH put AND EVERY DIRECTORY THROUGH mkdir, which is the point: this // adds a walk and no filesystem code at all, so anything the format refuses here it refuses // everywhere, in exactly the same words. static int compareEntries(const void *left, const void *right) { return strcmp(*(const char *const *)left, *(const char *const *)right); } static int mirrorDirectory(const char *path, const char *hostDir, const char *diskDir, int skipCount, char *const skips[]) { DIR *open = opendir(hostDir); if (open == NULL) { fprintf(stderr, "Error: Couldn't read the directory \"%s\".\n", hostDir); return 1; } // ---- Read the names first, and sort them ---- // // readdir hands them back in whatever order the host filesystem feels like, and a disk // image that comes out different from one run to the next is an image no test can // compare against another. Sorted, the same tree always makes the same disk. char **names = NULL; size_t count = 0, room = 0; const struct dirent *entry; while ((entry = readdir(open)) != NULL) { // Nothing beginning with a dot: that is . and .. and every editor's leavings, and // none of it is source anybody wants on the machine. if (entry->d_name[0] == '.') { continue; } int skipped = 0; for (int i = 0; i < skipCount; i++) { if (strcmp(entry->d_name, skips[i]) == 0) { skipped = 1; } } if (skipped) { continue; } if (count == room) { room = room ? room * 2 : 32; char **grown = realloc(names, room * sizeof(*names)); if (grown == NULL) { fprintf(stderr, "Error: Out of memory reading \"%s\".\n", hostDir); closedir(open); for (size_t i = 0; i < count; i++) free(names[i]); free(names); return 1; } names = grown; } names[count] = strdup(entry->d_name); if (names[count] == NULL) { fprintf(stderr, "Error: Out of memory reading \"%s\".\n", hostDir); closedir(open); for (size_t i = 0; i < count; i++) free(names[i]); free(names); return 1; } count++; } closedir(open); qsort(names, count, sizeof(*names), compareEntries); int failed = 0; for (size_t i = 0; i < count; i++) { char hostChild[1024]; char diskChild[1024]; if (snprintf(hostChild, sizeof(hostChild), "%s/%s", hostDir, names[i]) >= (int)sizeof(hostChild) || snprintf(diskChild, sizeof(diskChild), "%s/%s", diskDir, names[i]) >= (int)sizeof(diskChild)) { fprintf(stderr, "Error: \"%s/%s\" makes a path too long to follow.\n", hostDir, names[i]); failed = 1; continue; } // A NAME TOO LONG IS AN ERROR RATHER THAN A SKIP. Leaving it off would mean a build // that looks like it worked and a disk quietly missing a program, which is the exact // failure a mirror exists to prevent. Twenty-two bytes is what a directory entry // holds, and the fix is to call the file something shorter. if (strlen(names[i]) > SBFS_NAME_BYTES) { fprintf(stderr, "Error: \"%s\" is %zu characters, and a name holds %d.\n", names[i], strlen(names[i]), SBFS_NAME_BYTES); failed = 1; continue; } struct stat about; if (stat(hostChild, &about) != 0) { fprintf(stderr, "Error: Couldn't look at \"%s\".\n", hostChild); failed = 1; continue; } if (S_ISDIR(about.st_mode)) { if (commandMakeDirectory(path, diskChild) || mirrorDirectory(path, hostChild, diskChild, skipCount, skips)) { failed = 1; } } else if (S_ISREG(about.st_mode)) { if (commandPut(path, hostChild, diskChild)) { failed = 1; } } // Anything else - a socket, a device, whatever a host has - is not a thing this // filesystem has a way to be, so it is passed over without comment. } for (size_t i = 0; i < count; i++) { free(names[i]); } free(names); return failed; } static int commandMirror(const char *path, const char *hostDir, const char *diskDir, int skipCount, char *const skips[]) { struct stat about; if (stat(hostDir, &about) != 0 || !S_ISDIR(about.st_mode)) { fprintf(stderr, "Error: \"%s\" is not a directory to mirror.\n", hostDir); return 1; } return mirrorDirectory(path, hostDir, diskDir, skipCount, skips); } static int commandRemoveDirectory(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 = -1; const char *why = NULL; int failed = 1; if (resolve(&directory, name, &slot, &why)) { fprintf(stderr, "Error: \"%s\" %s.\n", name, why); goto done; } if (slot < 0) { fprintf(stderr, "Error: The root is not something that can be removed.\n"); goto done; } if (!entryIsDirectory(entryAt(&directory, slot))) { fprintf(stderr, "Error: \"%s\" is a file. Use delete.\n", name); goto done; } if (directoryHasChildren(&directory, slot)) { fprintf(stderr, "Error: \"%s\" still has things in it. Empty it first.\n", name); goto done; } memset(entryAt(&directory, slot), 0, SBFS_ENTRY_BYTES); super.freeBlocks = countFree(&directory, &super); if (writeDirectory(image, &super, &directory) || writeSuperblock(image, &super)) { goto done; } printf("Removed \"%s\".\n", name); failed = 0; done: free(directory.bytes); fclose(image); return failed; } static void printUsage(const char *program) { printf("Usage: %s [arguments]\n", program); printf("\n"); printf("Commands:\n"); printf(" format [blocks] [dirblocks] Lay down a fresh filesystem.\n"); printf(" list [path] Show the disk, or one directory of it.\n"); printf(" put [path] Put a host file onto it.\n"); printf(" get [file] Take one off it.\n"); printf(" delete Remove a file.\n"); printf(" mkdir Make a directory.\n"); printf(" rmdir Remove an empty one.\n"); printf(" mirror [skip...] Copy a whole host directory onto it,\n"); printf(" leaving behind anything named in skip.\n"); printf("\n"); printf("Blocks are %d bytes. A name may be %d characters, and a path is names with\n", SBFS_BLOCK_BYTES, SBFS_NAME_BYTES); printf("'%c' between them, always from the root. Without a path, put uses the file's\n", SBFS_SEPARATOR); printf("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; // Blocks in EACH boot slot, and there are two of them. Left off, a disk gets no // boot area at all, which is what every disk made before this had. long bootBlocks = (argc > 5) ? strtol(argv[5], NULL, 0) : 0; 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; } if (bootBlocks < 0 || bootBlocks * SBFS_BOOT_SLOTS > 0xFFFE) { fprintf(stderr, "Error: A boot slot is between 0 and %d blocks, and there are" " two of them.\n", 0xFFFE / SBFS_BOOT_SLOTS); return 1; } return commandFormat(path, (uint16_t)blocks, (uint16_t)directoryBlocks, (uint16_t)bootBlocks); } if (strcmp(command, "list") == 0) { return commandList(path, (argc > 3) ? argv[3] : NULL); } if (strcmp(command, "mkdir") == 0) { if (argc < 4) { fprintf(stderr, "Error: mkdir needs a directory to make.\n"); return 1; } return commandMakeDirectory(path, argv[3]); } if (strcmp(command, "rmdir") == 0) { if (argc < 4) { fprintf(stderr, "Error: rmdir needs a directory to remove.\n"); return 1; } return commandRemoveDirectory(path, argv[3]); } if (strcmp(command, "boot") == 0) { if (argc < 4) { fprintf(stderr, "Error: boot needs a file to write into a slot.\n"); return 1; } return commandBoot(path, argv[3], (argc > 4) ? strtol(argv[4], NULL, 0) : 0); } if (strcmp(command, "bootstate") == 0) { return commandBootState(path, (argc > 3) ? argv[3] : NULL); } if (strcmp(command, "bootslot") == 0) { if (argc < 4) { fprintf(stderr, "Error: bootslot needs the slot to start from.\n"); return 1; } return commandBootSlot(path, strtol(argv[3], NULL, 0)); } 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, "mirror") == 0) { if (argc < 5) { fprintf(stderr, "Error: mirror needs a directory to copy and somewhere to put" " it.\n"); return 1; } // Anything after those is a name to leave behind, which is how a project keeps what // it builds out of what it wrote. return commandMirror(path, argv[3], argv[4], argc - 5, &argv[5]); } 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; }