SchOS/kernel/fs/schfs/attr.c
Nexoniarz 26bca020b6 licence: SchOS is GPL-2.0-or-later, no longer Apache-2.0
Nexoniarz is the sole author and SchOS was never published under
Apache-2.0, so this is a straight relicence. LICENSE is the FSF GPLv2 text,
every first-party SPDX header moves to GPL-2.0-or-later, and NOTICE,
README.md, docs/licensing.md and the site say so. projects/schfs-linux
follows the rest of the tree.

The GPL gives copyleft: a fork has to ship its source. It also lifts the
old "Linux drivers, never" rule, since GPL-2.0-only code combines with
GPL-2.0-or-later. "Or later" leaves room for GPLv3, but one binary still
has to pick one version, so docs/licensing.md now keeps Apache-2.0 and
GPLv3 code out of the kernel to keep Linux code possible there. BSD
4-clause code goes from "avoid" to "no", because the advertising clause
is GPL-incompatible.

LDLS still ships no .ko. That boundary is now project policy rather than
a legal requirement, and ldls.md, devices.md and licensing.md section 6
say so. refmod declares license=GPL, and ldls-check passes (27 checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:48:45 +02:00

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 "types.h"
#include "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;
}