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>
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 "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;
|
|
}
|