SBFS version two adds directories out of space each entry had already set aside: two of the four reserved bytes become a parent, and one of the seven spare flag bits says an entry is a directory. The entry is still thirty two bytes, so it still divides two hundred and fifty six and still never straddles a block, and nothing in the block layer knows anything happened. A directory is an entry with no blocks. That is what keeps the flat array of entries the whole allocation map, which is the property the format is built on: 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. There is still no allocation table to consult and none to keep right. THE PARENT IS AN INDEX PLUS ONE, so zero means the root. A version one disk has zeroes in those bytes, and "in the root" is exactly where every file on a flat disk is - so a version one image is already a valid version two image, with nothing to convert and no tool to convert it with. A disk is at the lowest version that describes what is on it. format makes a version one disk and mkdir is what raises it, so everything built here stays readable by a reader that has never heard of a directory right up until it really does have one. That is what lets this land before the machine knows anything: the whole existing suite passes untouched. The tool gains mkdir and rmdir, and list, put, get and delete take paths. list also now reports entries used against entries available, because a disk has two ceilings and the entry one is the one nobody notices until it bites. rmdir refuses a directory with anything in it, and that is not politeness: parents are entry indices, a freed index gets handed out again, and the children of a removed directory would reappear inside whatever took its place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
123 lines
5.0 KiB
C
123 lines
5.0 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 Reserved to the end of the block
|
|
|
|
#define SBFS_SUPER_VERSION 4
|
|
#define SBFS_SUPER_DISK 6
|
|
#define SBFS_SUPER_DIRSTART 8
|
|
#define SBFS_SUPER_DIRBLOCKS 10
|
|
#define SBFS_SUPER_FREE 12
|
|
|
|
// ---- Directory entries ----
|
|
//
|
|
// 0 1 Flags
|
|
// 1 2 First block of the file's data
|
|
// 3 2 Whole blocks the file occupies
|
|
// 5 1 Bytes in the trailing part block, or zero if there is not one
|
|
// 6 22 Name, padded with zeroes
|
|
// 28 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
|
|
|
|
// 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)
|
|
|
|
// 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
|