Gate 1, item 3, and the last of foundations.md.
bind() creates a T_SOCK node at the path; connect() resolves the path and looks
for the socket bound to **that inode**. Matching on the inode rather than on
the path *string* is the part that matters: the name used to live only in the
socket table, so two spellings of one path missed each other, a relative path
never matched an absolute one, and there was nothing to stat() or unlink().
An inode is the thing a path resolves *to*, so connect() can walk the path
however it likes and arrive at the same socket.
# ls -l /tmp
srwxr-xr-x 1 root root 0 2026-09-22 08:04 socktest
The node is a marker, not the binding. It holds no data and the live
association stays in the socket table, so a socket file left by a crash is
what Linux leaves behind -- a dead file that connect() refuses until something
binds again -- and unlink()ing a live socket's path does not disturb anyone
already connected.
EADDRINUSE comes from the *directory* rather than from a scan of the table,
deliberately: a stale socket file has to refuse a rebind for the same reason a
running server's does, or two servers both believe they own it. The creation
lives in sys_bind() and not in unix.c, because that is the layer with both --
kernel/net/unix.c knows about sockets and has no business calling create().
T_SOCK is a compat addition to SchFS by the rule in schfs.h: a driver that does
not know SCHFS_T_SOCK maps it to a zero-length regular file, so nothing goes
stale and nothing becomes unreadable.
cmd/socktest section 15 asserts the plan's own wording -- bind, stat, see an
`s`, unlink -- plus the two a name table could never fail: a second bind to a
live path is refused, and connecting to a path that is a *regular file* is
refused rather than matched by name.
**And the semantics being real broke socktest on its second run**, correctly:
the socket files persisted and every bind() hit EADDRINUSE. Every real server
unlinks before binding; socktest now does too, which is the change the new
behaviour demands rather than a workaround for it.
Chasing that turned up something worse. `socktest` reported rc=1 with every
check passing, and so did every other first-party program:
- **lib/start.S called main and threw %eax away.** sys_exit() reads the
status from %rdi and nothing put it there, so every program exited with
whatever that register happened to hold. Every test suite ends in
`return failures ? 1 : 0` and none of it reached the shell.
- **exit(void) passed no status either**, for the same reason: the
declaration takes no argument, so %rdi was the caller's litter.
`if socktest; then` works now, and a failing command reports 1 -- checked both
ways, because a fix that makes everything return 0 would look identical from
the success side.
Also factored: unlink_path() out of sys_unlink, so a failed bind can take its
own node back out without going through the syscall entry, and create_sock()
as a named door rather than exporting create() with an arbitrary type.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
485 lines
23 KiB
C
485 lines
23 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>
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Records that live at an arbitrary offset inside a block.
|
|
//
|
|
// A leaf's value area holds variable-length records packed end to end, so a
|
|
// record begins wherever the one before it ended -- which is a multiple of 8
|
|
// only by luck. The code reads them by casting a pointer into the block, and
|
|
// that is a lie to the compiler unless it is told: the type says "8-byte
|
|
// aligned" and the pointer is not.
|
|
//
|
|
// It works on x86, which is why it never showed as a bug, and it is undefined
|
|
// in C. A compiler entitled to assume alignment may emit an instruction that
|
|
// requires it, and the failure would arrive at a different optimisation level
|
|
// or on a different architecture looking like filesystem corruption rather than
|
|
// like codegen. UndefinedBehaviorSanitizer reported 31 of these across obj.c,
|
|
// objw.c, attr.c and space.c.
|
|
//
|
|
// This annotation changes NO on-disk layout. Every struct it is applied to
|
|
// already carries explicit `pad` fields and therefore has no implicit padding;
|
|
// sizes and member offsets are identical with and without it, which was
|
|
// measured rather than assumed before it was added. What it changes is the
|
|
// code generated to read them: byte-wise where the compiler cannot prove
|
|
// alignment, which is exactly the truth here.
|
|
//
|
|
// It is deliberately NOT applied to schfs_node_hdr, schfs_item, schfs_kptr or
|
|
// schfs_uberblock. Those sit at fixed offsets in a block -- the header is 32
|
|
// bytes, so everything after it is 8-aligned -- and annotating them would cost
|
|
// byte-wise access on the hottest paths in the tree to fix a problem they do
|
|
// not have.
|
|
#define SCHFS_PACKED __attribute__((packed))
|
|
|
|
#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
|
|
#define SCHFS_KEY_SNAP 7 // (SNAP, SNAP, id) -> struct schfs_snap
|
|
#define SCHFS_KEY_EXTREF 8 // (EXTREF,EXTREF, diskblk) -> struct schfs_extref
|
|
|
|
// 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
|
|
// Snapshots live under their own reserved objectid, as ordinary records in the
|
|
// live tree. A snapshot is nothing more than a tree root that is kept: the
|
|
// B-tree is copy-on-write, so the blocks a root reaches are already immutable
|
|
// as long as nothing frees them. Keeping one costs a record.
|
|
#define SCHFS_SNAP_OBJECTID 2ULL
|
|
// Shared extents, keyed by the disk block a run starts at. btrfs calls this the
|
|
// extent tree and so, in effect, is this -- one reserved object id whose records
|
|
// say how many files point at a run.
|
|
//
|
|
// Only *shared* runs have a record. A run nothing has cloned has no entry at
|
|
// all, and absence means a reference count of one. That is not a space
|
|
// optimisation so much as a correctness one: the overwhelmingly common case
|
|
// stays exactly as it was, so the write path pays nothing for a feature it is
|
|
// not using, and there is no second structure to keep in step on every write.
|
|
#define SCHFS_EXTREF_OBJECTID 3ULL
|
|
|
|
// 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)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The on-disk format version, and what a bump means
|
|
//
|
|
// The rule, so that "can this driver read this volume" has an answer that does
|
|
// not depend on reading the release notes:
|
|
//
|
|
// * SCHFS_VERSION changes only when the *frame* changes -- the uberblock, the
|
|
// node header, the key, the things every reader must understand before it
|
|
// can parse anything at all. A driver refuses a version above its own,
|
|
// loudly, and says so; it does not guess and it does not fall back to an
|
|
// older uberblock, because an older uberblock is a stale filesystem.
|
|
//
|
|
// * Everything else is a FEATURE FLAG, and this is what keeps the version
|
|
// number from being a blunt instrument. Adding a record type, an index
|
|
// kind or an optional field does not break a reader that ignores it, so it
|
|
// must not cost every older reader the volume.
|
|
//
|
|
// Three classes, the arrangement ext4 and btrfs both arrived at because it is
|
|
// the one that works:
|
|
//
|
|
// compat Unknown bits may be ignored. Mount read-write regardless.
|
|
// ro_compat Unknown bits mean the volume may be MOUNTED READ-ONLY. The
|
|
// writer would maintain a structure this driver does not know
|
|
// about, so writing would silently invalidate it.
|
|
// incompat Unknown bits mean REFUSE. The volume cannot be parsed correctly
|
|
// without understanding them.
|
|
//
|
|
// Choosing the class is the whole design decision, and the test is simple:
|
|
// what happens if a driver that does not know this feature writes to the
|
|
// volume? Nothing -> compat. Something on disk goes stale -> ro_compat.
|
|
// Something is unreadable -> incompat.
|
|
//
|
|
// All three words are zero on every volume written so far: schfs_vol_commit
|
|
// memsets the uberblock before filling it in, so v1 images already say "no
|
|
// features" without having been written by code that knew the field existed.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#define SCHFS_VERSION 1
|
|
|
|
// No features are defined yet. The masks exist so that the *checks* exist:
|
|
// adding a flag later is one bit here, and every driver already refuses
|
|
// correctly for the bits it does not have.
|
|
#define SCHFS_FEAT_COMPAT_SUPP 0u
|
|
#define SCHFS_FEAT_RO_COMPAT_SUPP 0u
|
|
#define SCHFS_FEAT_INCOMPAT_SUPP 0u
|
|
|
|
#define SCHFS_LABEL_MAX 32
|
|
|
|
// A retained tree root.
|
|
//
|
|
// The root a snapshot names is a *previous* version of the live tree, and it is
|
|
// reached exactly as the live one is -- same node format, same checksums. What
|
|
// makes it survive is schfs_space_scan walking it too, so its blocks are never
|
|
// handed out again.
|
|
//
|
|
// The record is stored in the live tree, which means creating one rewrites the
|
|
// path to it under copy-on-write and produces a new live root. That is correct
|
|
// and is why the snapshot names the root as it was *before* the record was
|
|
// added: a snapshot that included its own creation would have to refer to a
|
|
// block that does not exist yet.
|
|
#define SCHFS_SNAP_NAME_MAX 64
|
|
|
|
// Set on a volume that has at least one snapshot. See schfs_space_free.
|
|
#define SCHFS_SPACE_RETAIN_ALL 1
|
|
|
|
struct schfs_snap {
|
|
uint64_t root; // block number of the retained tree root
|
|
uint64_t root_generation;
|
|
uint32_t root_crc; // checksum of that root block
|
|
uint32_t pad0;
|
|
uint64_t created; // generation the snapshot was taken at
|
|
char name[SCHFS_SNAP_NAME_MAX];
|
|
} SCHFS_PACKED;
|
|
|
|
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;
|
|
// Feature flags. See the block comment above SCHFS_VERSION for what the
|
|
// three classes mean and how to choose between them.
|
|
//
|
|
// These occupy what was one `uint64_t reserved0`, so the offset of every
|
|
// field after them is unchanged and a v1 image -- which has zeros here --
|
|
// reads identically. That is the only reason this could be added without a
|
|
// version bump, and it is the last time that trick is available.
|
|
uint32_t feature_incompat;
|
|
uint32_t feature_ro_compat;
|
|
uint64_t next_objectid;
|
|
uint64_t root_objectid; // normally SCHFS_ROOT_OBJECTID
|
|
uint8_t uuid[16];
|
|
char label[SCHFS_LABEL_MAX];
|
|
uint32_t feature_compat;
|
|
uint8_t pad[SCHFS_BLKSIZE - 140];
|
|
};
|
|
|
|
// The uberblock is one block, and every field above is at a fixed offset in it.
|
|
// Both are on-disk facts rather than C conveniences, so the compiler checks
|
|
// them: a padding change or a reordered field is a format change that would
|
|
// otherwise be found by a volume that no longer mounts.
|
|
_Static_assert(sizeof(struct schfs_uberblock) == SCHFS_BLKSIZE,
|
|
"uberblock must be exactly one block");
|
|
_Static_assert(__builtin_offsetof(struct schfs_uberblock, version) == 8,
|
|
"uberblock version moved -- older drivers read this offset");
|
|
_Static_assert(__builtin_offsetof(struct schfs_uberblock, feature_incompat) == 64,
|
|
"feature flags moved -- they must stay where reserved0 was");
|
|
_Static_assert(__builtin_offsetof(struct schfs_uberblock, next_objectid) == 72,
|
|
"fields after the feature flags must not shift");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Records
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#define SCHFS_T_FILE 1
|
|
#define SCHFS_T_DIR 2
|
|
#define SCHFS_T_DEV 3
|
|
#define SCHFS_T_SYMLINK 4
|
|
// A bound AF_UNIX socket. A *compat* addition by the rule above: a driver that
|
|
// does not know this value maps it to SCHFS_T_FILE and sees a zero-length
|
|
// regular file, which is harmless -- nothing goes stale and nothing becomes
|
|
// unreadable, so it needs no feature flag.
|
|
#define SCHFS_T_SOCK 5
|
|
|
|
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
|
|
} SCHFS_PACKED;
|
|
|
|
// 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;
|
|
// Whether this run is shared, as far as *this* file knows. A hint, not the
|
|
// truth: the count that decides whether blocks may be released lives in the
|
|
// extent-reference tree (SCHFS_KEY_EXTREF), because a per-file copy cannot
|
|
// be decremented by the other file that shares the run.
|
|
//
|
|
// Kept because it makes the common case free: refs == 1 here means nothing
|
|
// has ever cloned this run, so the write path can rewrite it in place
|
|
// without consulting the tree at all.
|
|
uint32_t refs;
|
|
uint32_t pad;
|
|
} SCHFS_PACKED;
|
|
|
|
// How many files point at a run of blocks. Present only for runs with more than
|
|
// one reference; see SCHFS_EXTREF_OBJECTID.
|
|
struct schfs_extref {
|
|
uint64_t nblocks;
|
|
uint64_t refs;
|
|
} SCHFS_PACKED;
|
|
|
|
// 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 */
|
|
} SCHFS_PACKED;
|
|
|
|
#define SCHFS_NAME_MAX 255
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Attributes, and the index over them
|
|
//
|
|
// An attribute is a named value on an object -- the same thing Linux calls an
|
|
// extended attribute, and the thing BeFS built its query interface on. Stored
|
|
// under (ino, ATTR, namehash), packed exactly as directory entries are, and for
|
|
// the same reason: two names can hash alike, and a filesystem that loses one of
|
|
// them under a collision is not one to build on.
|
|
//
|
|
// The point of having them is the *index*. An attribute may be indexed, and an
|
|
// indexed attribute gets a second set of records keyed by its value rather than
|
|
// by the object -- so "every object whose `type` is `photo`" is a range scan of
|
|
// a few keys rather than a walk of every object in the filesystem. That is the
|
|
// whole of what "a filesystem you can query" means here, and the reason the
|
|
// design is one B-tree with many key types: the feature is a few hundred lines
|
|
// on machinery that already exists, rather than a second engine.
|
|
struct schfs_attr_rec {
|
|
uint32_t namelen;
|
|
uint32_t valuelen;
|
|
uint16_t type; // SCHFS_ATTR_* below; what a query compares as
|
|
uint16_t flags;
|
|
uint32_t pad;
|
|
/* name bytes, then value bytes; the pair padded to an 8-byte boundary */
|
|
} SCHFS_PACKED;
|
|
|
|
// What a value *is*, so that a query can compare two of them meaningfully. A
|
|
// string and an integer that happen to share bytes are not the same value, and
|
|
// ordering differs between them.
|
|
#define SCHFS_ATTR_RAW 0 // opaque bytes; only equality is meaningful
|
|
#define SCHFS_ATTR_STRING 1
|
|
#define SCHFS_ATTR_INT 2 // int64, little-endian
|
|
#define SCHFS_ATTR_TIME 3 // seconds since the epoch, int64
|
|
|
|
#define SCHFS_ATTR_NAME_MAX 63
|
|
#define SCHFS_ATTR_VALUE_MAX 255
|
|
|
|
// Step from one packed attribute record to the next. 8-byte aligned so the
|
|
// uint32s of the next one are naturally aligned.
|
|
#define SCHFS_ATTR_LEN(namelen, valuelen) \
|
|
((unsigned)((sizeof(struct schfs_attr_rec) + (namelen) + (valuelen) + 7) & ~7u))
|
|
|
|
// An index entry: one object that carries a given value for a given attribute.
|
|
//
|
|
// Keyed (indexid, INDEX, valuehash), where indexid is the hash of the attribute
|
|
// *name*. Several objects can share a value, so the payload is a packed list of
|
|
// these, exactly as dirents and attributes are.
|
|
struct schfs_index_rec {
|
|
uint64_t objectid;
|
|
uint32_t valuelen;
|
|
uint32_t pad;
|
|
/* value bytes follow, so a query can reject a hash collision */
|
|
} SCHFS_PACKED;
|
|
|
|
#define SCHFS_INDEX_LEN(valuelen) \
|
|
((unsigned)((sizeof(struct schfs_index_rec) + (valuelen) + 7) & ~7u))
|
|
|
|
// Which attributes are indexed. One record, under the index object id, whose
|
|
// value is a packed list of attribute names.
|
|
//
|
|
// A list rather than a flag on each attribute, because the question asked at
|
|
// every setxattr is "is *this name* indexed", and answering it from one small
|
|
// record is a single cached lookup rather than a search.
|
|
#define SCHFS_INDEX_OBJECTID 4ULL
|
|
#define SCHFS_INDEX_LIST_OFF 0ULL /* the one record's offset in the key */
|
|
|
|
// 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);
|
|
SCHFS_ASSERT_SIZE(schfs_extref, 16);
|
|
SCHFS_ASSERT_SIZE(schfs_attr_rec, 16);
|
|
SCHFS_ASSERT_SIZE(schfs_index_rec, 16);
|
|
|
|
#endif /* SCHFS_H */
|
|
|
|
// The annotation above must not have moved anything. These are on-disk sizes:
|
|
// if one changes, every existing volume is misread from that record onward, and
|
|
// the failure looks like corruption rather than like a struct that grew.
|
|
_Static_assert(sizeof(struct schfs_dirent_rec) == 16, "dirent record size changed");
|
|
_Static_assert(sizeof(struct schfs_extent) == 32, "extent record size changed");
|
|
_Static_assert(sizeof(struct schfs_extref) == 16, "extref record size changed");
|
|
_Static_assert(sizeof(struct schfs_index_rec) == 16, "index record size changed");
|
|
_Static_assert(sizeof(struct schfs_attr_rec) == 16, "attr record size changed");
|
|
_Static_assert(sizeof(struct schfs_snap) == 96, "snapshot record size changed");
|
|
_Static_assert(sizeof(struct schfs_inode) == 80, "inode record size changed");
|