Files
Anachronaut dc74149321 B4: the disk remembers whether the last start arrived
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.
2026-08-27 16:51:39 -04:00

213 lines
10 KiB
C

// sbfs.h
// The SplitBit Filesystem, version two.
//
// This is the host side's copy of the format. The other implementation is SplitBit
// assembly running on the machine itself, so nothing can be shared between them except
// the specification: the two have to be kept honest by a document rather than by a
// header. Everything here follows that document exactly, and anything that changes here
// has to change there in the same breath.
//
// All multi byte numbers are most significant byte first, the same as every other number
// SplitBit stores: addresses, the SPBT boot image header, and the vector table.
//
// Written by Anachronaut
#ifndef SBFS_H
#define SBFS_H
#include <stdint.h>
#define SBFS_MAGIC "SBFS"
#define SBFS_MAGIC_BYTES 4
// ---- Versions ----
//
// Version two adds directories, and adds them without moving a single byte that version
// one had defined: the parent lives in two of the four bytes each entry already reserved,
// and a directory is a flag bit in a byte that was using one of its eight.
//
// A VERSION ONE DISK IS ALREADY A VALID VERSION TWO DISK. The parent field is written as
// the entry's index PLUS ONE, so that the zero a version one disk has in those reserved
// bytes reads as "in the root" - which is exactly what every file on a flat disk is in.
// There is nothing to convert and no tool to convert it with.
//
// Compatibility therefore runs one way, which is the ordinary shape of it: this reads
// either version, and version one code reading a version two disk would list directories
// as strange empty files. A disk is written back at the version it was read at, and is
// only raised to two by the thing that makes the difference real - the first directory
// created on it.
#define SBFS_VERSION_FLAT 1
#define SBFS_VERSION_TREE 2
#define SBFS_VERSION SBFS_VERSION_TREE
#define SBFS_BLOCK_BYTES 256
// ---- Block 0, the superblock ----
//
// 0 4 "SBFS"
// 4 1 Version
// 5 1 Reserved
// 6 2 Blocks on the disk
// 8 2 First directory block
// 10 2 Blocks the directory occupies
// 12 2 Free blocks, a cache rather than the authority
// 14 2 Blocks in each boot slot, or zero for a disk that cannot be booted
// 16 1 Which boot slot is live, 0 or 1
// 17 1 How the last start went. See below.
// 18 Reserved to the end of the block
#define SBFS_SUPER_VERSION 4
#define SBFS_SUPER_DISK 6
#define SBFS_SUPER_DIRSTART 8
#define SBFS_SUPER_DIRBLOCKS 10
#define SBFS_SUPER_FREE 12
#define SBFS_SUPER_BOOTBLOCKS 14
#define SBFS_SUPER_BOOTSLOT 16
// ---- The boot area ----
//
// Blocks between the superblock and the directory, which the filesystem never allocates
// and never sees. Nothing had to be added to make room for them: both implementations
// work out the first usable data block as directoryStart + directoryBlocks, and
// directoryStart is a field rather than a constant, so moving the directory up reserves
// everything below it by arithmetic that was already there.
//
// A DISK MADE BEFORE THIS HAS ZERO HERE, which reads as "no boot area", which is true.
// The same shape as the version two parent field: the value an older disk already holds
// is the correct answer rather than something needing conversion.
//
// TWO SLOTS, ALWAYS, and the reason is that a boot slot is raw blocks. A file being
// rewritten is protected by writing a temporary and renaming it, and there is no name
// here to rename - so a machine interrupted while updating its only boot slot would not
// boot at all, which is the one failure on this disk with no way back. Writing the slot
// that is not live and then moving one byte turns that into a machine that boots the
// version it had before.
//
// block 0 the superblock
// 1 .. bootBlocks slot 0
// bootBlocks+1 .. 2*bootBlocks slot 1
// directoryStart .. the directory, and then files
// ---- How the last start went ----
//
// Written by the loader before it hands over and cleared by the system once it is running,
// so that a system which never gets that far leaves a mark saying so. THE MARK IS WHAT
// MAKES A NEW SYSTEM SAFE TO TRY: without it, pointing boot.cfg at something that crashes
// before the prompt is a machine that cannot be told anything ever again.
//
// What clears it is reaching the shell, and that is a deliberate choice of threshold. It
// does not mean the system works - a shell can be reached by something that is broken in
// every other way. It means A PERSON HAS CONTROL AGAIN, which is exactly what the fallback
// exists to restore and therefore exactly when it has done its job.
//
// 0 Settled. The last start finished. Start what the configuration says.
// 1 Trying. The loader handed over and nothing came back to say it arrived.
// 2 Fell back. A try failed and the fallback was used instead. Stays until somebody
// settles it, so that a system which crashes is not retried every other
// boot for ever.
#define SBFS_SUPER_BOOTSTATE 17
#define SBFS_BOOT_SETTLED 0
#define SBFS_BOOT_TRYING 1
#define SBFS_BOOT_FELLBACK 2
#define SBFS_BOOT_SLOTS 2
#define SBFS_FIRST_BOOT_BLOCK 1
// ---- Directory entries ----
//
// 0 1 Flags
// 1 2 First block of the file's data
// 3 2 Whole blocks the file occupies
// 5 1 Bytes in the trailing part block, or zero if there is not one
// 6 22 Name, padded with zeroes
// 28 2 Parent, as an entry index plus one. Zero is the root. (Version two.)
// 30 2 Reserved
//
// Thirty two divides two hundred and fifty six, so an entry never straddles a block and
// reading one never means handling a split. Version two did not change that, because it
// spent bytes that were already inside the entry.
//
// A DIRECTORY IS AN ENTRY WITH NO BLOCKS. Its start, blocks and tail are all zero, and it
// costs one entry and nothing else. That is what keeps the flat array of entries the
// whole allocation map: with files laid down contiguously, every block is inside some
// entry's range or it is not, and an entry with no range is in nobody's way.
//
// Because parents are entry indices, and because nothing ever compacts the directory,
// those indices are stable for as long as an entry is in use. DELETING A DIRECTORY THAT
// STILL HAS CHILDREN MUST BE REFUSED: the freed index would be handed to some unrelated
// file later, and the orphans would reappear inside it.
#define SBFS_ENTRY_BYTES 32
#define SBFS_ENTRIES_PER_BLOCK (SBFS_BLOCK_BYTES / SBFS_ENTRY_BYTES)
#define SBFS_ENTRY_FLAGS 0
#define SBFS_ENTRY_START 1
#define SBFS_ENTRY_BLOCKS 3
#define SBFS_ENTRY_TAIL 5
#define SBFS_ENTRY_NAME 6
#define SBFS_NAME_BYTES 22
#define SBFS_ENTRY_PARENT 28
#define SBFS_FLAG_IN_USE 0x01
#define SBFS_FLAG_DIRECTORY 0x02
// A FILE BEING WRITTEN, WHICH IS NOT YET A FILE. Saving something that already exists is
// done by writing a temporary, deleting the original and giving the temporary its name,
// so that nothing is lost if the writing fails. The temporary has to be an ordinary entry
// while that happens - it holds real blocks and needs a name - and the only thing that
// distinguishes it from a finished file is that nobody has committed it yet.
//
// That is not a property of its contents. The same bytes become the real file the moment
// the rename lands, so there is nothing to put inside it that would be true; it belongs
// in the entry, which is the thing the commit changes. It was formerly told apart by
// being called "sbfs.part" or "sbfs.out", and those are legal names a user may also
// choose, so cleaning up by name could delete somebody's file.
//
// Cleared as part of committing. An entry still carrying it is the wreckage of a write
// that stopped, and its blocks are spoken for until something clears it up.
#define SBFS_FLAG_TEMPORARY 0x04
// The root is not an entry. It is the absence of a parent, written as zero, which is why
// the field is an index plus one and why a freshly zeroed entry is already in the root.
#define SBFS_PARENT_ROOT 0
#define SBFS_PARENT_OF(index) ((uint16_t)((index) + 1))
#define SBFS_PARENT_INDEX(parent) ((int)(parent) - 1)
// ---- How big a directory may be ----
//
// The parent is an index plus one in sixteen bits, so index 65535 has no representation:
// adding one wraps to zero, and zero is the root. An entry that cannot be named as a
// parent is a directory that cannot hold anything, and it does not fail by refusing.
//
// WHAT IT DOES INSTEAD IS WORSE THAN FAILING. Creating something inside it writes a
// parent of zero, so the thing lands in the root while the tool reports the path it was
// asked for. Looking in that directory afterwards finds nothing, because the search is
// for a parent of 65536 and the entry says zero - so the same create succeeds again, and
// again, piling up entries of one name in the root. Duplicate names in one directory are
// the one thing rename refuses outright, on the grounds that a search answers with
// whichever it meets first and the rest can never be reached; this manufactured them.
//
// Eight entries to a block, and 65535 entries is the most that leaves every index one
// short of the wrap. 8191 blocks gives 65528 of them, which is the last whole block that
// fits. Checked when formatting and again when reading, because a disk claiming more may
// have been made by something that never checked at all.
#define SBFS_MAX_DIRECTORY_BLOCKS 8191
#define SBFS_MAX_ENTRIES (SBFS_MAX_DIRECTORY_BLOCKS * SBFS_ENTRIES_PER_BLOCK)
// Paths are separated by this, and a leading one means "from the root". A name may not
// contain it, which is what makes a path unambiguous without any quoting.
#define SBFS_SEPARATOR '/'
// The directory begins at block 1 and is this many blocks unless told otherwise, which
// is sixty four files. The superblock carries the real number, so this is only what a
// freshly formatted disk gets.
#define SBFS_FIRST_DIRECTORY_BLOCK 1
#define SBFS_DEFAULT_DIRECTORY_BLOCKS 8
// A file of n bytes occupies n / 256 whole blocks and, if anything is left over, one more
// for the tail. Zero means zero in both, so an empty file occupies nothing at all.
#define SBFS_WHOLE_BLOCKS(bytes) ((bytes) / SBFS_BLOCK_BYTES)
#define SBFS_TAIL_BYTES(bytes) ((bytes) % SBFS_BLOCK_BYTES)
#define SBFS_BLOCKS_USED(blocks, tail) ((blocks) + ((tail) ? 1 : 0))
#endif // SBFS_H