The loader marks the superblock before it hands over and the system clears the mark when it reaches its prompt, so a system that crashes on the way there leaves it set. The loader finding it still set next time is how a machine that will not start says so to the only thing in a position to do anything about it. Without that, pointing boot.cfg at something that dies before the shell is a machine that can never be told anything again - the shell is the only way to change the file, and the file is what stops the shell from starting. Three states rather than two, and the third is the one worth having: 0 settled the last start arrived; use the configuration 1 trying handed over, and nothing came back to say it got there 2 fell back a try failed and the fallback was used, until settled With only 0 and 1 the machine alternates for ever: fall back, reach a prompt, clear the mark, retry the broken system, crash, fall back. State 2 stops that. A system known not to start is not tried again until somebody says the situation has changed. REACHING THE PROMPT IS A DELIBERATE THRESHOLD. It is not a claim that the system works - a shell can be reached by something broken in every other way. It is the point where a person can type, which is exactly what the fallback exists to give back: anything wrong past there is fixable from the prompt and nothing wrong before it is fixable at all. The routines live in sbfs.asm because both the loader and the system read and write this byte, and two pieces of code with their own idea of where a byte lives is what this format has two implementations and a byte for byte comparison to avoid. And the trap this system documents in its own manual caught me anyway: the first version handed the state back in A, which CALL restores, so every read got whatever the caller happened to be holding. It comes back in memory now, and the comment says why. Three disks differing only in the state on them, so the tests read as three consecutive starts of one machine while none depends on another running.
1270 lines
48 KiB
C
1270 lines
48 KiB
C
// 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 {
|
|
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.
|
|
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 <command> <image> [arguments]\n", program);
|
|
printf("\n");
|
|
printf("Commands:\n");
|
|
printf(" format <image> [blocks] [dirblocks] Lay down a fresh filesystem.\n");
|
|
printf(" list <image> [path] Show the disk, or one directory of it.\n");
|
|
printf(" put <image> <file> [path] Put a host file onto it.\n");
|
|
printf(" get <image> <path> [file] Take one off it.\n");
|
|
printf(" delete <image> <path> Remove a file.\n");
|
|
printf(" mkdir <image> <path> Make a directory.\n");
|
|
printf(" rmdir <image> <path> Remove an empty one.\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, "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;
|
|
}
|