SchOS/include/schfs.h
Nexoniarz d0f7acf9eb Initial commit: Schunix as it stands before the SchOS rename
An x86-64 operating system built from xv6, now largely rewritten: SMP,
a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF
rasteriser, and a Linux-numbered syscall ABI.

No history before this point -- the tree was never under version control.
This commit exists so the rename that follows has something to undo to.
2026-08-09 13:47:01 +02:00

246 lines
10 KiB
C

// SchFS — on-disk format.
//
// Design rationale lives in docs/architecture/schfs.md. In short: copy-on-write
// with the checksum of a block stored in the pointer that leads to it (ZFS),
// one B-tree reused for every kind of record (btrfs), and attribute indexes as
// just another record type (BeFS).
//
// This header is shared by the kernel and by host tools (tools/mkfs,
// tools/schfs_test), so it must stay free of kernel-only types.
#ifndef SCHFS_H
#define SCHFS_H
#include <stdint.h>
#define SCHFS_MAGIC 0x53636846u // "SchF"
#define SCHFS_NODE_MAGIC 0x536e6f64u // "Snod"
// 4096 to match modern media and make extents worth having. The old format
// used 1024, which is also why its directories were so wasteful.
#define SCHFS_BLKSIZE 4096
// Deepest tree we will walk without recursing. 8 levels of a ~84-way internal
// node is far more than any plausible volume needs.
#define SCHFS_MAX_LEVEL 8
// ---------------------------------------------------------------------------
// Keys
//
// Every record in the filesystem is addressed by one 24-byte key, sorted by
// (objectid, type, offset). This is the whole reason there is only one tree:
// inodes, directory entries, extent maps and indexes are just different `type`
// values in the same keyspace.
// ---------------------------------------------------------------------------
#define SCHFS_KEY_INODE 1 // (ino, INODE, 0) -> struct schfs_inode
#define SCHFS_KEY_DIRENT 2 // (dirino,DIRENT, namehash) -> packed dirent list
#define SCHFS_KEY_EXTENT 3 // (ino, EXTENT, fileblk) -> struct schfs_extent
#define SCHFS_KEY_ATTR 4 // (ino, ATTR, namehash) -> attribute value
#define SCHFS_KEY_INDEX 5 // (idxid, INDEX, valuehash) -> objectid
#define SCHFS_KEY_CSUM 6 // (CSUM, CSUM, diskblk) -> uint32 CRC32c
// Data-block checksums live under one reserved object id, exactly as btrfs
// keeps a checksum tree. Tree nodes carry their checksum in their parent
// pointer, but file *data* blocks have no parent to hold it — without these
// records "end-to-end checksums" would only cover metadata, which is the part
// least likely to rot.
#define SCHFS_CSUM_OBJECTID 1ULL
// Object ids below this are reserved; the root directory is the first real one.
#define SCHFS_FIRST_OBJECTID 16ULL
#define SCHFS_ROOT_OBJECTID 16ULL
struct schfs_key {
uint64_t objectid;
uint64_t offset;
uint8_t type;
uint8_t pad[7];
}; /* 24 bytes */
// ---------------------------------------------------------------------------
// Node header — identical for internal nodes and leaves.
//
// `blocknr` is a self-pointer: a block that was written to the wrong place
// still checksums correctly, so the only way to catch a misdirected write is
// for the block to say where it thinks it lives.
// ---------------------------------------------------------------------------
struct schfs_node_hdr {
uint32_t magic; // SCHFS_NODE_MAGIC
uint32_t crc; // CRC32c of the whole block, this field zeroed
uint64_t blocknr; // where this block believes it lives
uint64_t generation; // transaction that wrote it
uint16_t level; // 0 = leaf, >0 = internal
uint16_t nritems;
uint32_t pad;
}; /* 32 bytes */
// Internal node entry: a key and the child it leads to, plus that child's
// checksum and generation. Storing the checksum *here* rather than in the
// child is what makes the tree self-validating.
struct schfs_kptr {
struct schfs_key key;
uint64_t blocknr;
uint64_t generation;
uint32_t crc;
uint32_t pad;
}; /* 48 bytes */
// Leaf entry. Items grow forward from the end of the header; their data grows
// backward from the end of the block, exactly like btrfs. `offset` is measured
// from the start of the block.
struct schfs_item {
struct schfs_key key;
uint32_t offset;
uint32_t size;
}; /* 32 bytes */
#define SCHFS_NODE_SPACE (SCHFS_BLKSIZE - (int)sizeof(struct schfs_node_hdr))
#define SCHFS_MAX_KPTR (SCHFS_NODE_SPACE / (int)sizeof(struct schfs_kptr))
#define SCHFS_MAX_ITEMS (SCHFS_NODE_SPACE / (int)sizeof(struct schfs_item))
// Largest value that can live inline in a leaf. Anything bigger becomes an
// extent record pointing at data blocks.
#define SCHFS_MAX_INLINE (SCHFS_NODE_SPACE / 4)
// ---------------------------------------------------------------------------
// Uberblock
//
// A ring of these is written round-robin. Mounting means reading them all and
// taking the highest generation that checksums — that is the entire crash
// recovery story, in place of a journal replay.
// ---------------------------------------------------------------------------
#define SCHFS_UBER_COUNT 128
#define SCHFS_UBER_START 1 // block 0 stays free for a boot sector
// First block available for the tree and for file data: everything below is
// the boot block and the uberblock ring.
#define SCHFS_FIRST_BLOCK (SCHFS_UBER_START + SCHFS_UBER_COUNT)
#define SCHFS_VERSION 1
#define SCHFS_LABEL_MAX 32
struct schfs_uberblock {
uint32_t magic; // SCHFS_MAGIC
uint32_t crc; // CRC32c of this struct, this field zeroed
uint32_t version; // SCHFS_VERSION
uint32_t blocksize; // SCHFS_BLKSIZE; a reader must refuse a mismatch
uint64_t generation; // transaction number; highest valid one wins
uint64_t root; // block number of the tree root
uint32_t root_crc; // checksum of that root block
uint32_t pad0;
uint64_t root_generation;
uint64_t total_blocks;
// Informational only. Which blocks are actually free is derived from the
// tree at mount rather than stored, so this is what the volume believed at
// commit time — useful for df before anything is mounted, never trusted.
uint64_t free_blocks;
uint64_t reserved0;
uint64_t next_objectid;
uint64_t root_objectid; // normally SCHFS_ROOT_OBJECTID
uint8_t uuid[16];
char label[SCHFS_LABEL_MAX];
uint8_t pad[SCHFS_BLKSIZE - 136];
};
// ---------------------------------------------------------------------------
// Records
// ---------------------------------------------------------------------------
#define SCHFS_T_FILE 1
#define SCHFS_T_DIR 2
#define SCHFS_T_DEV 3
#define SCHFS_T_SYMLINK 4
struct schfs_inode {
uint64_t size;
uint64_t blocks;
uint64_t atime, mtime, ctime;
// Containing directory. Meaningful for directories only, where it is what
// ".." resolves to — neither "." nor ".." is stored on disk, so a directory
// costs nothing until it has real entries. For files with several hard
// links "parent" is simply the first directory that linked them and must
// not be trusted.
uint64_t parent;
uint32_t mode; // real POSIX mode bits, at last
uint32_t uid, gid;
uint32_t nlink;
uint16_t type; // SCHFS_T_*
uint16_t rdev_major, rdev_minor;
uint16_t pad;
uint32_t flags; // SCHFS_I_* — none defined yet
uint32_t gen; // transaction that last changed this inode;
// what an incremental send/snapshot diff needs
};
// An extent maps a run of file blocks onto a run of disk blocks. refs is what
// makes snapshots and reflink copies nearly free: sharing a range is a refcount
// bump rather than a data copy.
struct schfs_extent {
uint64_t disk_block;
uint64_t nblocks;
// Transaction group that allocated this run. A run stamped with the group
// currently being built cannot be reached from any committed root, so its
// blocks may be overwritten in place — which is what stops a program that
// writes a hundred bytes at a time from copying a whole 4 KB block, and
// then a whole tree path, on every call.
//
// A full 64 bits, matching the tree's generation counter, because a 32-bit
// one would wrap after four billion commits and a wrapped comparison says
// "same group" about a block belonging to a committed tree — the one
// mistake this field exists to prevent. Only ever trusted together with
// refs == 1; a shared run belongs to somebody else as well.
uint64_t gen;
uint32_t refs;
uint32_t pad;
};
// Directory entry payload. The key's offset field holds the name hash, so a
// lookup is one tree descent rather than a directory scan.
//
// Two names in one directory can hash the same, and the key would then be
// identical — the second insert would silently replace the first. So the value
// is a *packed list*: one schfs_dirent_rec plus its name bytes, repeated. A
// 64-bit hash makes lists longer than one vanishingly rare, but "vanishingly
// rare" is not "impossible", and a filesystem that loses a file under a hash
// collision is not one to build on.
struct schfs_dirent_rec {
uint64_t objectid;
uint16_t type;
uint16_t namelen;
uint32_t pad;
/* name bytes follow, namelen of them, not NUL-terminated */
};
#define SCHFS_NAME_MAX 255
// Step from one packed dirent to the next. Records are 8-byte aligned so the
// uint64 objectid of the next one is naturally aligned.
#define SCHFS_DIRENT_LEN(namelen) \
((unsigned)((sizeof(struct schfs_dirent_rec) + (namelen) + 7) & ~7u))
// ---------------------------------------------------------------------------
// Compile-time layout checks.
//
// Every one of these is a number that ends up baked into images on disk. A
// silent change to any of them — adding a field, changing a type, a compiler
// padding a struct differently — makes every existing image unreadable in a way
// that looks like corruption rather than like a mistake. Failing the build is
// enormously cheaper than debugging that.
// ---------------------------------------------------------------------------
#define SCHFS_ASSERT_SIZE(t, n) \
typedef char schfs_sizecheck_##t[(sizeof(struct t) == (n)) ? 1 : -1]
SCHFS_ASSERT_SIZE(schfs_key, 24);
SCHFS_ASSERT_SIZE(schfs_node_hdr, 32);
SCHFS_ASSERT_SIZE(schfs_kptr, 48);
SCHFS_ASSERT_SIZE(schfs_item, 32);
SCHFS_ASSERT_SIZE(schfs_uberblock, SCHFS_BLKSIZE);
SCHFS_ASSERT_SIZE(schfs_inode, 80);
SCHFS_ASSERT_SIZE(schfs_extent, 32);
SCHFS_ASSERT_SIZE(schfs_dirent_rec, 16);
#endif /* SCHFS_H */