Sixty-five headers sat side by side in include/, so `#include "irq.h"` said nothing about where irq.h was or what it belonged to, and errno.h, stat.h and fcntl.h at the top shadowed the system's for any host tool built with -I./include. They are in folders now -- arch/ core/ fs/ dev/ gfx/ net/ uapi/ (and ldls/ for the Linux driver layer) -- and every include names the folder, as Linux's do: "core/proc.h". Done mechanically and checked the hard way: the kernel built from this tree disassembles identically to the one built from the tree before it. The full image boots, and userland is the same set of programs. Paths in docs and comments follow; docs/guides/layout.md has a table of what is where, and tools/fix-includes.py rewrites old-style includes from the folders in the tree, for code written before this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
507 lines
18 KiB
C
507 lines
18 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
//
|
|
// Attributes, and the index over them.
|
|
//
|
|
// An attribute is a named value on an object. The interesting part is not
|
|
// storing them -- that is another key type in a tree that already exists -- but
|
|
// the index: an indexed attribute gets a second set of records keyed by *value*
|
|
// rather than by object, so "every object whose `tag` is `holiday`" is a range
|
|
// scan of a few keys instead of a walk of the whole filesystem.
|
|
//
|
|
// That is the whole of what "a filesystem you can query" means here, and the
|
|
// reason the design was one B-tree with many key types from the start. There is
|
|
// no query engine, no parser and no planner; there is an index, which is the
|
|
// part that actually makes the question fast. See docs/architecture/schfs.md §6.
|
|
//
|
|
// ---------------------------------------------------------------------------
|
|
// Packed lists, again
|
|
//
|
|
// Attributes are keyed (ino, ATTR, hash-of-name) and index entries
|
|
// (name-hash, INDEX, hash-of-value). Both hash something that can collide, so
|
|
// both store a *packed list* of records rather than a single one -- the same
|
|
// shape directory entries use, for the same reason. A 64-bit hash makes a
|
|
// collision vanishingly unlikely and "vanishingly unlikely" is not "impossible".
|
|
|
|
#include "schfs_int.h"
|
|
|
|
#ifdef SCHFS_HOSTED
|
|
#include <string.h>
|
|
#else
|
|
#include "core/types.h"
|
|
#include "core/defs.h"
|
|
#endif
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Reading */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
// Find one attribute in the packed list under its name's hash.
|
|
//
|
|
// Returns 1 and fills the caller's buffers if present, 0 if absent, -2 on
|
|
// corruption. `*valuelen` is set to the value's true length even when the
|
|
// buffer is too small, so a caller can size a second call -- which is what
|
|
// getxattr(2) promises.
|
|
int
|
|
schfs_attr_get(struct schfs_vol *v, uint64_t ino, const char *name,
|
|
unsigned namelen, void *out, unsigned cap, unsigned *valuelen,
|
|
uint16_t *type)
|
|
{
|
|
if (namelen == 0 || namelen > SCHFS_ATTR_NAME_MAX)
|
|
return 0;
|
|
|
|
struct schfs_key k;
|
|
schfs_mkkey(&k, ino, SCHFS_KEY_ATTR, schfs_namehash(name, namelen));
|
|
|
|
unsigned len = SCHFS_MAX_INLINE;
|
|
int r = schfs_btree_lookup(&v->tree, &v->ctx, &k, v->dirbuf, &len);
|
|
if (r == -1)
|
|
return 0;
|
|
if (r != 0)
|
|
return r;
|
|
if (len > SCHFS_MAX_INLINE)
|
|
return -2;
|
|
|
|
unsigned off = 0;
|
|
while (off + sizeof(struct schfs_attr_rec) <= len) {
|
|
const struct schfs_attr_rec *a =
|
|
(const struct schfs_attr_rec *)(v->dirbuf + off);
|
|
unsigned step = SCHFS_ATTR_LEN(a->namelen, a->valuelen);
|
|
if (a->namelen == 0 || a->namelen > SCHFS_ATTR_NAME_MAX ||
|
|
a->valuelen > SCHFS_ATTR_VALUE_MAX || off + step > len)
|
|
return -2;
|
|
|
|
const char *nm = (const char *)a + sizeof(*a);
|
|
if (a->namelen == namelen && memcmp(nm, name, namelen) == 0) {
|
|
if (valuelen) *valuelen = a->valuelen;
|
|
if (type) *type = a->type;
|
|
if (out && cap >= a->valuelen)
|
|
memmove(out, nm + a->namelen, a->valuelen);
|
|
else if (out)
|
|
return -3; /* buffer too small; length is set */
|
|
return 1;
|
|
}
|
|
off += step;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Hand every attribute on an object to a callback, in the order they are
|
|
// stored. Used by listxattr and by the reindexing walk.
|
|
int
|
|
schfs_attr_iterate(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
|
|
int (*fn)(const char *name, unsigned namelen,
|
|
const void *val, unsigned vallen,
|
|
uint16_t type, void *arg),
|
|
void *arg)
|
|
{
|
|
struct schfs_key start;
|
|
schfs_mkkey(&start, ino, SCHFS_KEY_ATTR, 0);
|
|
|
|
// Re-point at the volume's current tree. The root moves on every commit
|
|
// under copy-on-write, so a cursor is bound per operation rather than once.
|
|
schfs_cursor_init(c, &v->tree, c->blk);
|
|
if (schfs_cursor_seek(c, &start) != 0)
|
|
return 0;
|
|
|
|
for (;;) {
|
|
const struct schfs_key *k = schfs_cursor_key(c);
|
|
if (!k || k->objectid != ino || k->type != SCHFS_KEY_ATTR)
|
|
return 0;
|
|
|
|
unsigned len;
|
|
const char *val = (const char *)schfs_cursor_val(c, &len);
|
|
if (!val || len > SCHFS_MAX_INLINE)
|
|
return -2;
|
|
|
|
// Copied out before the callback runs: the callback may look something
|
|
// up, and that reuses the very buffer this points into. The same trap
|
|
// that once reparented directories under their own children.
|
|
memmove(v->dirbuf, val, len);
|
|
|
|
unsigned off = 0;
|
|
while (off + sizeof(struct schfs_attr_rec) <= len) {
|
|
const struct schfs_attr_rec *a =
|
|
(const struct schfs_attr_rec *)(v->dirbuf + off);
|
|
unsigned step = SCHFS_ATTR_LEN(a->namelen, a->valuelen);
|
|
if (a->namelen == 0 || a->namelen > SCHFS_ATTR_NAME_MAX ||
|
|
a->valuelen > SCHFS_ATTR_VALUE_MAX || off + step > len)
|
|
return -2;
|
|
const char *nm = (const char *)a + sizeof(*a);
|
|
int stop = fn(nm, a->namelen, nm + a->namelen, a->valuelen,
|
|
a->type, arg);
|
|
if (stop)
|
|
return 0;
|
|
off += step;
|
|
}
|
|
|
|
if (schfs_cursor_next(c) != 0)
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* The index */
|
|
/* */
|
|
/* Which attributes are indexed is one record, and it is consulted on */
|
|
/* every attribute write -- so it is read into the volume rather than */
|
|
/* fetched each time. See schfs_index_reload. */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
// Is `name` an indexed attribute?
|
|
//
|
|
// Answered from the cached list rather than the tree. setxattr asks this every
|
|
// time, and the answer is almost always no; a tree descent to say so would make
|
|
// every attribute write pay for a feature it is not using.
|
|
static int
|
|
index_wanted(struct schfs_vol *v, const char *name, unsigned namelen)
|
|
{
|
|
unsigned off = 0;
|
|
while (off + 1 <= v->idxnames_len) {
|
|
unsigned l = (unsigned char)v->idxnames[off];
|
|
if (l == 0 || off + 1 + l > v->idxnames_len)
|
|
return 0;
|
|
if (l == namelen && memcmp(v->idxnames + off + 1, name, namelen) == 0)
|
|
return 1;
|
|
off += 1 + l;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Read the indexed-attribute list off the disk into the volume.
|
|
//
|
|
// Called at mount and after any change to the list. A volume with no indexes --
|
|
// which is every volume until somebody asks for one -- ends up with an empty
|
|
// list and index_wanted answers no without touching anything.
|
|
int
|
|
schfs_index_reload(struct schfs_vol *v)
|
|
{
|
|
struct schfs_key k;
|
|
schfs_mkkey(&k, SCHFS_INDEX_OBJECTID, SCHFS_KEY_INDEX, SCHFS_INDEX_LIST_OFF);
|
|
|
|
unsigned len = sizeof(v->idxnames);
|
|
int r = schfs_btree_lookup(&v->tree, &v->ctx, &k, v->idxnames, &len);
|
|
if (r == -1) {
|
|
v->idxnames_len = 0;
|
|
return 0;
|
|
}
|
|
if (r != 0)
|
|
return r;
|
|
v->idxnames_len = len;
|
|
return 0;
|
|
}
|
|
|
|
// Add one object to the index for (attribute name, value).
|
|
static int
|
|
index_add(struct schfs_vol *v, const char *name, unsigned namelen,
|
|
const void *val, unsigned vallen, uint64_t ino)
|
|
{
|
|
struct schfs_key k;
|
|
schfs_mkkey(&k, schfs_namehash(name, namelen), SCHFS_KEY_INDEX,
|
|
schfs_namehash((const char *)val, vallen));
|
|
|
|
unsigned len = SCHFS_MAX_INLINE;
|
|
int r = schfs_btree_lookup(&v->tree, &v->ctx, &k, v->dirbuf, &len);
|
|
if (r == -1)
|
|
len = 0;
|
|
else if (r != 0)
|
|
return r;
|
|
|
|
// Already listed? An object that sets the same value twice must not appear
|
|
// twice, or a query would return it twice.
|
|
unsigned off = 0;
|
|
while (off + sizeof(struct schfs_index_rec) <= len) {
|
|
const struct schfs_index_rec *ir =
|
|
(const struct schfs_index_rec *)(v->dirbuf + off);
|
|
unsigned step = SCHFS_INDEX_LEN(ir->valuelen);
|
|
if (off + step > len)
|
|
return -2;
|
|
if (ir->objectid == ino)
|
|
return 0;
|
|
off += step;
|
|
}
|
|
|
|
unsigned add = SCHFS_INDEX_LEN(vallen);
|
|
if (len + add > SCHFS_MAX_INLINE)
|
|
return -3; /* too many objects share this value */
|
|
|
|
struct schfs_index_rec ir;
|
|
memset(&ir, 0, sizeof(ir));
|
|
ir.objectid = ino;
|
|
ir.valuelen = vallen;
|
|
memmove(v->dirbuf + len, &ir, sizeof(ir));
|
|
memmove(v->dirbuf + len + sizeof(ir), val, vallen);
|
|
// The padding is zeroed rather than left as whatever was in the buffer, so
|
|
// two identical filesystems produce byte-identical images.
|
|
memset(v->dirbuf + len + sizeof(ir) + vallen, 0,
|
|
add - sizeof(ir) - vallen);
|
|
|
|
return schfs_btree_insert(&v->tree, &v->ctx, &k, v->dirbuf, len + add);
|
|
}
|
|
|
|
// Remove one object from the index for (attribute name, value).
|
|
static int
|
|
index_del(struct schfs_vol *v, const char *name, unsigned namelen,
|
|
const void *val, unsigned vallen, uint64_t ino)
|
|
{
|
|
struct schfs_key k;
|
|
schfs_mkkey(&k, schfs_namehash(name, namelen), SCHFS_KEY_INDEX,
|
|
schfs_namehash((const char *)val, vallen));
|
|
|
|
unsigned len = SCHFS_MAX_INLINE;
|
|
int r = schfs_btree_lookup(&v->tree, &v->ctx, &k, v->dirbuf, &len);
|
|
if (r == -1)
|
|
return 0;
|
|
if (r != 0)
|
|
return r;
|
|
|
|
unsigned off = 0;
|
|
while (off + sizeof(struct schfs_index_rec) <= len) {
|
|
struct schfs_index_rec *ir = (struct schfs_index_rec *)(v->dirbuf + off);
|
|
unsigned step = SCHFS_INDEX_LEN(ir->valuelen);
|
|
if (off + step > len)
|
|
return -2;
|
|
if (ir->objectid == ino) {
|
|
memmove(v->dirbuf + off, v->dirbuf + off + step, len - off - step);
|
|
len -= step;
|
|
if (len == 0)
|
|
return schfs_btree_delete(&v->tree, &v->ctx, &k);
|
|
return schfs_btree_insert(&v->tree, &v->ctx, &k, v->dirbuf, len);
|
|
}
|
|
off += step;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Writing */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
// Set an attribute, replacing any previous value of the same name.
|
|
//
|
|
// The index is kept in step here rather than by a separate pass, because an
|
|
// index that can be out of date is worse than none: a query would answer
|
|
// confidently and wrongly.
|
|
int
|
|
schfs_attr_set(struct schfs_vol *v, uint64_t ino, const char *name,
|
|
unsigned namelen, const void *val, unsigned vallen,
|
|
uint16_t type)
|
|
{
|
|
if (namelen == 0 || namelen > SCHFS_ATTR_NAME_MAX)
|
|
return -1;
|
|
if (vallen > SCHFS_ATTR_VALUE_MAX)
|
|
return -1;
|
|
|
|
schfs_cache_invalidate();
|
|
|
|
// The old value has to come out of the index before the new one goes in,
|
|
// and it has to be read before the record is overwritten.
|
|
int indexed = index_wanted(v, name, namelen);
|
|
char oldval[SCHFS_ATTR_VALUE_MAX];
|
|
unsigned oldlen = 0;
|
|
int had_old = 0;
|
|
if (indexed) {
|
|
unsigned vl = 0;
|
|
int g = schfs_attr_get(v, ino, name, namelen, oldval, sizeof(oldval),
|
|
&vl, 0);
|
|
if (g == 1) { had_old = 1; oldlen = vl; }
|
|
}
|
|
|
|
struct schfs_key k;
|
|
schfs_mkkey(&k, ino, SCHFS_KEY_ATTR, schfs_namehash(name, namelen));
|
|
|
|
unsigned len = SCHFS_MAX_INLINE;
|
|
int r = schfs_btree_lookup(&v->tree, &v->ctx, &k, v->dirbuf, &len);
|
|
if (r == -1)
|
|
len = 0;
|
|
else if (r != 0)
|
|
return r;
|
|
|
|
// Drop any record with this name; what remains is everything that merely
|
|
// hashed the same.
|
|
unsigned off = 0;
|
|
while (off + sizeof(struct schfs_attr_rec) <= len) {
|
|
struct schfs_attr_rec *a = (struct schfs_attr_rec *)(v->dirbuf + off);
|
|
unsigned step = SCHFS_ATTR_LEN(a->namelen, a->valuelen);
|
|
if (a->namelen == 0 || off + step > len)
|
|
return -2;
|
|
const char *nm = (const char *)a + sizeof(*a);
|
|
if (a->namelen == namelen && memcmp(nm, name, namelen) == 0) {
|
|
memmove(v->dirbuf + off, v->dirbuf + off + step, len - off - step);
|
|
len -= step;
|
|
break;
|
|
}
|
|
off += step;
|
|
}
|
|
|
|
unsigned add = SCHFS_ATTR_LEN(namelen, vallen);
|
|
if (len + add > SCHFS_MAX_INLINE)
|
|
return -3;
|
|
|
|
struct schfs_attr_rec a;
|
|
memset(&a, 0, sizeof(a));
|
|
a.namelen = namelen;
|
|
a.valuelen = vallen;
|
|
a.type = type;
|
|
memmove(v->dirbuf + len, &a, sizeof(a));
|
|
memmove(v->dirbuf + len + sizeof(a), name, namelen);
|
|
memmove(v->dirbuf + len + sizeof(a) + namelen, val, vallen);
|
|
memset(v->dirbuf + len + sizeof(a) + namelen + vallen, 0,
|
|
add - sizeof(a) - namelen - vallen);
|
|
|
|
r = schfs_btree_insert(&v->tree, &v->ctx, &k, v->dirbuf, len + add);
|
|
if (r != 0)
|
|
return r;
|
|
|
|
if (indexed) {
|
|
if (had_old) {
|
|
r = index_del(v, name, namelen, oldval, oldlen, ino);
|
|
if (r != 0)
|
|
return r;
|
|
}
|
|
r = index_add(v, name, namelen, val, vallen, ino);
|
|
if (r != 0)
|
|
return r;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Remove an attribute. Absent is not a failure.
|
|
int
|
|
schfs_attr_del(struct schfs_vol *v, uint64_t ino, const char *name,
|
|
unsigned namelen)
|
|
{
|
|
if (namelen == 0 || namelen > SCHFS_ATTR_NAME_MAX)
|
|
return -1;
|
|
|
|
schfs_cache_invalidate();
|
|
|
|
int indexed = index_wanted(v, name, namelen);
|
|
char oldval[SCHFS_ATTR_VALUE_MAX];
|
|
unsigned oldlen = 0;
|
|
int had_old = 0;
|
|
if (indexed) {
|
|
unsigned vl = 0;
|
|
int g = schfs_attr_get(v, ino, name, namelen, oldval, sizeof(oldval),
|
|
&vl, 0);
|
|
if (g == 1) { had_old = 1; oldlen = vl; }
|
|
}
|
|
|
|
struct schfs_key k;
|
|
schfs_mkkey(&k, ino, SCHFS_KEY_ATTR, schfs_namehash(name, namelen));
|
|
|
|
unsigned len = SCHFS_MAX_INLINE;
|
|
int r = schfs_btree_lookup(&v->tree, &v->ctx, &k, v->dirbuf, &len);
|
|
if (r == -1)
|
|
return 0;
|
|
if (r != 0)
|
|
return r;
|
|
|
|
unsigned off = 0;
|
|
int found = 0;
|
|
while (off + sizeof(struct schfs_attr_rec) <= len) {
|
|
struct schfs_attr_rec *a = (struct schfs_attr_rec *)(v->dirbuf + off);
|
|
unsigned step = SCHFS_ATTR_LEN(a->namelen, a->valuelen);
|
|
if (a->namelen == 0 || off + step > len)
|
|
return -2;
|
|
const char *nm = (const char *)a + sizeof(*a);
|
|
if (a->namelen == namelen && memcmp(nm, name, namelen) == 0) {
|
|
memmove(v->dirbuf + off, v->dirbuf + off + step, len - off - step);
|
|
len -= step;
|
|
found = 1;
|
|
break;
|
|
}
|
|
off += step;
|
|
}
|
|
if (!found)
|
|
return 0;
|
|
|
|
r = (len == 0) ? schfs_btree_delete(&v->tree, &v->ctx, &k)
|
|
: schfs_btree_insert(&v->tree, &v->ctx, &k, v->dirbuf, len);
|
|
if (r != 0)
|
|
return r;
|
|
|
|
if (indexed && had_old)
|
|
return index_del(v, name, namelen, oldval, oldlen, ino);
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Declaring an index, and querying it */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
// Start indexing an attribute.
|
|
//
|
|
// Only affects values written *after* this: there is no reindexing pass, and
|
|
// pretending otherwise would be the kind of half-truth that makes a query
|
|
// answer confidently and wrongly. A caller that wants existing objects in the
|
|
// index rewrites their attributes.
|
|
int
|
|
schfs_index_create(struct schfs_vol *v, const char *name, unsigned namelen)
|
|
{
|
|
if (namelen == 0 || namelen > SCHFS_ATTR_NAME_MAX)
|
|
return -1;
|
|
|
|
schfs_cache_invalidate();
|
|
|
|
if (index_wanted(v, name, namelen))
|
|
return 0; /* already indexed */
|
|
|
|
if (v->idxnames_len + 1 + namelen > sizeof(v->idxnames))
|
|
return -3;
|
|
|
|
v->idxnames[v->idxnames_len] = (char)namelen;
|
|
memmove(v->idxnames + v->idxnames_len + 1, name, namelen);
|
|
v->idxnames_len += 1 + namelen;
|
|
|
|
struct schfs_key k;
|
|
schfs_mkkey(&k, SCHFS_INDEX_OBJECTID, SCHFS_KEY_INDEX, SCHFS_INDEX_LIST_OFF);
|
|
return schfs_btree_insert(&v->tree, &v->ctx, &k, v->idxnames,
|
|
v->idxnames_len);
|
|
}
|
|
|
|
// Every object whose `name` attribute equals `val`.
|
|
//
|
|
// One tree descent, whatever the size of the filesystem -- which is the entire
|
|
// point. Returns how many object ids were written to `out`, or negative on
|
|
// error. A caller with a full buffer gets `cap` and no way to ask for more,
|
|
// which is a limit worth having rather than an unbounded kernel allocation.
|
|
int
|
|
schfs_query_equal(struct schfs_vol *v, const char *name, unsigned namelen,
|
|
const void *val, unsigned vallen, uint64_t *out, int cap)
|
|
{
|
|
if (namelen == 0 || namelen > SCHFS_ATTR_NAME_MAX)
|
|
return -1;
|
|
if (!index_wanted(v, name, namelen))
|
|
return -1; /* not indexed: there is nothing to scan */
|
|
|
|
struct schfs_key k;
|
|
schfs_mkkey(&k, schfs_namehash(name, namelen), SCHFS_KEY_INDEX,
|
|
schfs_namehash((const char *)val, vallen));
|
|
|
|
unsigned len = SCHFS_MAX_INLINE;
|
|
int r = schfs_btree_lookup(&v->tree, &v->ctx, &k, v->dirbuf, &len);
|
|
if (r == -1)
|
|
return 0; /* no object carries that value */
|
|
if (r != 0)
|
|
return r;
|
|
|
|
int n = 0;
|
|
unsigned off = 0;
|
|
while (off + sizeof(struct schfs_index_rec) <= len && n < cap) {
|
|
const struct schfs_index_rec *ir =
|
|
(const struct schfs_index_rec *)(v->dirbuf + off);
|
|
unsigned step = SCHFS_INDEX_LEN(ir->valuelen);
|
|
if (off + step > len)
|
|
return -2;
|
|
|
|
// The value is stored, not just its hash, and this is why: two values
|
|
// can hash alike, and a query that returned the wrong file would be a
|
|
// far worse failure than a slow one.
|
|
if (ir->valuelen == vallen &&
|
|
memcmp((const char *)ir + sizeof(*ir), val, vallen) == 0)
|
|
out[n++] = ir->objectid;
|
|
|
|
off += step;
|
|
}
|
|
return n;
|
|
}
|