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>
587 lines
20 KiB
C
587 lines
20 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
//
|
|
// SchFS object layer, read side: inodes, directories and file data.
|
|
//
|
|
// Everything here is a record in the one B-tree, which is the point of the
|
|
// btrfs-style design — there is no inode table, no directory block format and
|
|
// no indirect-block scheme to get wrong, only key types:
|
|
//
|
|
// (ino, INODE, 0) struct schfs_inode
|
|
// (dir, DIRENT, hash) packed list of schfs_dirent_rec + name
|
|
// (ino, EXTENT, fileblk) struct schfs_extent
|
|
// (CSUM, CSUM, diskblk) uint32 CRC32c of that data block
|
|
//
|
|
// Kernel code reaches all of this through a caller-supplied cursor, so no
|
|
// function here puts a 4096-byte block on the stack.
|
|
|
|
#include "schfs_int.h"
|
|
|
|
#ifdef SCHFS_HOSTED
|
|
#include <string.h>
|
|
#else
|
|
#include "types.h"
|
|
#include "defs.h"
|
|
#endif
|
|
|
|
// Point a cursor at the volume's current tree, keeping the caller's block
|
|
// buffer. Called before every operation rather than once at open, because the
|
|
// tree root moves on every commit under copy-on-write: a cursor that cached the
|
|
// old root would keep reading a stale — though perfectly valid — filesystem.
|
|
static void
|
|
cur_bind(struct schfs_cursor *c, struct schfs_vol *v)
|
|
{
|
|
c->t = &v->tree;
|
|
c->depth = 0;
|
|
c->valid = 0;
|
|
}
|
|
|
|
void
|
|
schfs_mkkey(struct schfs_key *k, uint64_t objectid, uint8_t type,
|
|
uint64_t offset)
|
|
{
|
|
memset(k, 0, sizeof(*k));
|
|
k->objectid = objectid;
|
|
k->type = type;
|
|
k->offset = offset;
|
|
}
|
|
|
|
// FNV-1a, 64-bit. Directory lookup is a tree descent on this value, so it wants
|
|
// to spread short, highly similar names ("ls", "ln", "cp") across the keyspace.
|
|
// FNV does that, is a dozen lines, and needs no table — worth more here than
|
|
// the marginally better distribution of something larger.
|
|
//
|
|
// Collisions are handled by the packed dirent list rather than assumed away;
|
|
// see struct schfs_dirent_rec.
|
|
// Test-only: narrows the hash so collisions become common.
|
|
//
|
|
// With a full 64-bit hash, two names in one directory colliding is so unlikely
|
|
// that the packed-list code which handles it would never run — and untested
|
|
// code that only executes on rare inputs is exactly the code that loses a file
|
|
// years later. Setting this to a small mask makes every directory a pile of
|
|
// collisions, so the round-trip test exercises the path properly. Always ~0
|
|
// in the kernel and in real images.
|
|
uint64_t schfs_hash_mask = ~0ULL;
|
|
|
|
uint64_t
|
|
schfs_namehash(const char *name, unsigned len)
|
|
{
|
|
uint64_t h = 1469598103934665603ULL; /* FNV offset basis */
|
|
for (unsigned i = 0; i < len; i++) {
|
|
h ^= (uint8_t)name[i];
|
|
h *= 1099511628211ULL; /* FNV prime */
|
|
}
|
|
// Key ordering is by this value, so a directory's entries come back in an
|
|
// order that has nothing to do with their names. That is fine for readdir
|
|
// (POSIX promises no order) and is what makes lookup one descent.
|
|
return h & schfs_hash_mask;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Inodes */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* Inode cache.
|
|
*
|
|
* Sixteen entries, not one. `ls` is the shape that matters: it stats every name
|
|
* in a directory, so a single entry never hits and the whole directory costs
|
|
* two tree descents per file -- one to find the name, one to read the inode.
|
|
* Sixteen is enough that a directory walk keeps hitting while a second reader
|
|
* elsewhere does not evict everything.
|
|
*
|
|
* Round-robin replacement, deliberately: an LRU needs bookkeeping on every hit,
|
|
* and the access pattern here is a walk rather than a working set, so the
|
|
* cleverer policy buys nothing.
|
|
*
|
|
* Invalidation is blunt and total -- see schfs_cache_invalidate().
|
|
*/
|
|
#define INO_CACHE_N 16
|
|
|
|
static struct {
|
|
int valid;
|
|
const struct schfs_vol *vol;
|
|
uint64_t ino;
|
|
struct schfs_inode in;
|
|
} ino_cache[INO_CACHE_N];
|
|
|
|
static unsigned ino_cache_next;
|
|
|
|
int
|
|
schfs_inode_get(struct schfs_vol *v, struct schfs_cursor *c,
|
|
uint64_t ino, struct schfs_inode *out)
|
|
{
|
|
for (unsigned i = 0; i < INO_CACHE_N; i++) {
|
|
if (ino_cache[i].valid && ino_cache[i].vol == v &&
|
|
ino_cache[i].ino == ino) {
|
|
*out = ino_cache[i].in;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
struct schfs_key k;
|
|
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
|
|
|
|
cur_bind(c, v);
|
|
unsigned len = sizeof(*out);
|
|
int r = schfs_btree_lookup_cur(c, &k, out, &len);
|
|
if (r != 0)
|
|
return r;
|
|
// A short record means an image written by an older or broken tool. Reject
|
|
// it rather than acting on a half-filled struct.
|
|
if (len != sizeof(*out))
|
|
return -2;
|
|
|
|
unsigned slot = ino_cache_next++ % INO_CACHE_N;
|
|
ino_cache[slot].valid = 1;
|
|
ino_cache[slot].vol = v;
|
|
ino_cache[slot].ino = ino;
|
|
ino_cache[slot].in = *out;
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
schfs_snap_get(struct schfs_vol *v, struct schfs_cursor *c, uint64_t id,
|
|
struct schfs_snap *out)
|
|
{
|
|
struct schfs_key k;
|
|
schfs_mkkey(&k, SCHFS_SNAP_OBJECTID, SCHFS_KEY_SNAP, id);
|
|
|
|
cur_bind(c, v);
|
|
unsigned len = sizeof(*out);
|
|
int r = schfs_btree_lookup_cur(c, &k, out, &len);
|
|
if (r != 0)
|
|
return r;
|
|
if (len != sizeof(*out))
|
|
return -2;
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Directories */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
// Walk a packed dirent list, calling fn per entry. Stops early if fn returns
|
|
// non-zero. Returns fn's value, or -2 if the list is malformed.
|
|
static int
|
|
dirent_walk(const void *val, unsigned len,
|
|
int (*fn)(const struct schfs_dirent_rec *, const char *, void *),
|
|
void *arg)
|
|
{
|
|
unsigned off = 0;
|
|
while (off < len) {
|
|
if (len - off < sizeof(struct schfs_dirent_rec))
|
|
return -2;
|
|
const struct schfs_dirent_rec *d =
|
|
(const struct schfs_dirent_rec *)((const char *)val + off);
|
|
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
|
|
// Bounds-check before touching the name: a corrupt namelen would
|
|
// otherwise read past the end of the block.
|
|
if (d->namelen == 0 || d->namelen > SCHFS_NAME_MAX || off + step > len)
|
|
return -2;
|
|
int r = fn(d, (const char *)d + sizeof(*d), arg);
|
|
if (r)
|
|
return r;
|
|
off += step;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
struct find_ctx {
|
|
const char *name;
|
|
unsigned namelen;
|
|
uint64_t ino;
|
|
uint16_t type;
|
|
};
|
|
|
|
static int
|
|
find_one(const struct schfs_dirent_rec *d, const char *nm, void *arg)
|
|
{
|
|
struct find_ctx *f = (struct find_ctx *)arg;
|
|
if (d->namelen != f->namelen)
|
|
return 0;
|
|
if (memcmp(nm, f->name, f->namelen) != 0)
|
|
return 0;
|
|
f->ino = d->objectid;
|
|
f->type = d->type;
|
|
return 1; /* found; stop */
|
|
}
|
|
|
|
int
|
|
schfs_dir_lookup(struct schfs_vol *v, struct schfs_cursor *c,
|
|
uint64_t dir, const char *name, unsigned namelen,
|
|
uint64_t *out_ino, uint16_t *out_type)
|
|
{
|
|
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
|
|
return -1;
|
|
|
|
struct schfs_key k;
|
|
schfs_mkkey(&k, dir, SCHFS_KEY_DIRENT, schfs_namehash(name, namelen));
|
|
|
|
cur_bind(c, v);
|
|
int r = schfs_cursor_seek(c, &k);
|
|
if (r == -2)
|
|
return -2;
|
|
if (r != 0)
|
|
return -1;
|
|
|
|
const struct schfs_key *got = schfs_cursor_key(c);
|
|
if (!got || schfs_key_cmp(got, &k) != 0)
|
|
return -1;
|
|
|
|
unsigned len;
|
|
const void *val = schfs_cursor_val(c, &len);
|
|
if (!val)
|
|
return -2;
|
|
|
|
struct find_ctx f;
|
|
f.name = name; f.namelen = namelen; f.ino = 0; f.type = 0;
|
|
r = dirent_walk(val, len, find_one, &f);
|
|
if (r == -2)
|
|
return -2;
|
|
if (r != 1)
|
|
return -1; /* hash matched, name did not */
|
|
|
|
if (out_ino) *out_ino = f.ino;
|
|
if (out_type) *out_type = f.type;
|
|
return 0;
|
|
}
|
|
|
|
struct iter_ctx {
|
|
// The hash-aware form. schfs_dir_iterate's plain callback is wrapped in one
|
|
// of these, so there is one walk rather than two.
|
|
schfs_dir_fn fn;
|
|
void *arg;
|
|
struct schfs_cursor *cur;
|
|
uint64_t dir;
|
|
int stop;
|
|
};
|
|
|
|
// Hand each entry of one packed dirent list to the caller.
|
|
//
|
|
// The list lives in the cursor's block buffer, and almost every readdir caller
|
|
// immediately looks each entry up — to stat it, to recurse into it — through a
|
|
// cursor that may well share that buffer. Walking the list in place meant the
|
|
// name and every record after it turned to garbage the moment the callback
|
|
// touched the filesystem, which is how an extracted tree once ended up with
|
|
// directories reparented under their own children.
|
|
//
|
|
// The obvious fix, copying the whole item aside, costs SCHFS_MAX_INLINE (just
|
|
// over 1 KB) of stack — a quarter of the kernel's entire 4096-byte stack, on a
|
|
// path that also runs under procread's 2 KB buffer. So only the current entry
|
|
// is copied, and the item is re-fetched from the cursor each time round:
|
|
// schfs_cursor_val re-reads the block, which restores whatever the callback
|
|
// disturbed.
|
|
static int
|
|
iter_item(const struct schfs_key *k, const void *val, unsigned len, void *arg)
|
|
{
|
|
struct iter_ctx *ic = (struct iter_ctx *)arg;
|
|
(void)val;
|
|
|
|
// The range scan runs until something stops it, so stopping at the first
|
|
// key that is no longer this directory's is what bounds the walk.
|
|
if (k->objectid != ic->dir || k->type != SCHFS_KEY_DIRENT)
|
|
return 1;
|
|
if (len > SCHFS_MAX_INLINE)
|
|
return 1;
|
|
|
|
unsigned off = 0;
|
|
while (off < len) {
|
|
unsigned now;
|
|
const char *v = (const char *)schfs_cursor_val(ic->cur, &now);
|
|
if (!v || now != len) {
|
|
ic->stop = -2;
|
|
return 1;
|
|
}
|
|
if (len - off < sizeof(struct schfs_dirent_rec)) {
|
|
ic->stop = -2;
|
|
return 1;
|
|
}
|
|
|
|
const struct schfs_dirent_rec *d =
|
|
(const struct schfs_dirent_rec *)(v + off);
|
|
unsigned namelen = d->namelen;
|
|
unsigned step = SCHFS_DIRENT_LEN(namelen);
|
|
// Bounds-check before touching the name: a corrupt namelen would
|
|
// otherwise read past the end of the block.
|
|
if (namelen == 0 || namelen > SCHFS_NAME_MAX || off + step > len) {
|
|
ic->stop = -2;
|
|
return 1;
|
|
}
|
|
|
|
char name[SCHFS_NAME_MAX];
|
|
uint64_t oid = d->objectid;
|
|
uint16_t type = d->type;
|
|
memmove(name, (const char *)d + sizeof(*d), namelen);
|
|
|
|
int r = ic->fn(oid, type, name, namelen, k->offset, ic->arg);
|
|
if (r) {
|
|
ic->stop = r;
|
|
return 1;
|
|
}
|
|
off += step;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Walk a directory from a given point in its key space.
|
|
//
|
|
// `start_hash` is where to resume: entries are keyed (dir, DIRENT, hash), so
|
|
// seeking straight to a hash is what makes a partial listing continue in one
|
|
// descent instead of re-walking everything before it. See schfs_vfs_getdents.
|
|
int
|
|
schfs_dir_iterate_from(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir,
|
|
uint64_t start_hash, schfs_dir_fn fn, void *arg)
|
|
{
|
|
struct schfs_key start;
|
|
schfs_mkkey(&start, dir, SCHFS_KEY_DIRENT, start_hash);
|
|
|
|
struct iter_ctx ic;
|
|
ic.fn = fn; ic.arg = arg; ic.cur = c; ic.dir = dir; ic.stop = 0;
|
|
|
|
cur_bind(c, v);
|
|
int r = schfs_btree_range(c, &start, iter_item, &ic);
|
|
if (r == -2)
|
|
return -2;
|
|
return ic.stop == 1 ? 0 : ic.stop;
|
|
}
|
|
|
|
// The plain form, for callers that want the whole directory and do not care
|
|
// where in the key space an entry sat.
|
|
struct plain_ctx {
|
|
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *);
|
|
void *arg;
|
|
};
|
|
|
|
static int
|
|
plain_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen,
|
|
uint64_t hash, void *arg)
|
|
{
|
|
struct plain_ctx *p = arg;
|
|
(void)hash;
|
|
return p->fn(ino, type, name, namelen, p->arg);
|
|
}
|
|
|
|
int
|
|
schfs_dir_iterate(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir,
|
|
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *),
|
|
void *arg)
|
|
{
|
|
struct plain_ctx p = { fn, arg };
|
|
return schfs_dir_iterate_from(v, c, dir, 0, plain_entry, &p);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* File data */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* --------------------------------------------------------------------------
|
|
* Read caches: recently-read inodes (see schfs_inode_get) and the last extent.
|
|
*
|
|
* Every read used to descend the B-tree twice per block -- once for the inode
|
|
* and once for the extent -- so an 834 KB file cost 418 descents, each a tree
|
|
* walk plus a 4 KB copy out of the buffer cache per node visited. Sequential
|
|
* reads ask for the same inode every time and usually stay inside one extent,
|
|
* so both answers are already known.
|
|
*
|
|
* One entry each, not a table: the access pattern being optimised is one file
|
|
* read straight through, and a single entry catches all of it without needing
|
|
* eviction policy or a hash. A second reader simply misses.
|
|
*
|
|
* Correctness rests entirely on invalidation, so it is deliberately blunt:
|
|
* every write entry point in objw.c calls schfs_cache_invalidate() before it
|
|
* touches anything. A cache that is merely usually right is worse than none at
|
|
* all here -- a stale extent hands back another file's data.
|
|
*
|
|
* The volume pointer is part of the key because the host tools can hold more
|
|
* than one volume open, and two images would otherwise share these.
|
|
* ------------------------------------------------------------------------ */
|
|
static struct {
|
|
int valid;
|
|
const struct schfs_vol *vol;
|
|
uint64_t ino;
|
|
uint64_t first; /* first file block covered */
|
|
uint64_t nblocks; /* how many, from `first` */
|
|
uint64_t disk; /* disk block backing `first` */
|
|
} ext_cache;
|
|
|
|
void
|
|
schfs_cache_invalidate(void)
|
|
{
|
|
// Retires every cursor's resident block as well as the two caches below:
|
|
// an in-place rewrite leaves a stale copy that is indistinguishable from
|
|
// the real thing by inspection, so the only safe signal is "something
|
|
// changed". See cur_load in btree.c.
|
|
schfs_tree_epoch++;
|
|
for (unsigned i = 0; i < INO_CACHE_N; i++)
|
|
ino_cache[i].valid = 0;
|
|
ext_cache.valid = 0;
|
|
}
|
|
|
|
// Map a file block to a disk block. Returns 1 if mapped, 0 for a hole, -2 on
|
|
// corruption. Extents are keyed by the file block they start at, so the record
|
|
// governing block B is the last one at or before (ino, EXTENT, B).
|
|
static int
|
|
extent_map(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
|
|
uint64_t fblk, uint64_t *out)
|
|
{
|
|
if (ext_cache.valid && ext_cache.vol == v && ext_cache.ino == ino &&
|
|
fblk >= ext_cache.first && fblk < ext_cache.first + ext_cache.nblocks) {
|
|
*out = ext_cache.disk + (fblk - ext_cache.first);
|
|
return 1;
|
|
}
|
|
|
|
struct schfs_key k;
|
|
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
|
|
|
|
cur_bind(c, v);
|
|
int r = schfs_btree_lookup_le(c, &k);
|
|
if (r == -2)
|
|
return -2;
|
|
if (r != 0)
|
|
return 0;
|
|
|
|
const struct schfs_key *got = schfs_cursor_key(c);
|
|
if (!got)
|
|
return 0;
|
|
// lookup_le can land on some other object's records entirely; a sparse file
|
|
// whose first extent starts past this block looks the same way.
|
|
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
|
|
return 0;
|
|
|
|
unsigned len;
|
|
const struct schfs_extent *e =
|
|
(const struct schfs_extent *)schfs_cursor_val(c, &len);
|
|
if (!e || len != sizeof(*e))
|
|
return -2;
|
|
|
|
if (fblk >= got->offset + e->nblocks)
|
|
return 0; /* hole past the end of this extent */
|
|
|
|
ext_cache.valid = 1;
|
|
ext_cache.vol = v;
|
|
ext_cache.ino = ino;
|
|
ext_cache.first = got->offset;
|
|
ext_cache.nblocks = e->nblocks;
|
|
ext_cache.disk = e->disk_block;
|
|
|
|
*out = e->disk_block + (fblk - got->offset);
|
|
return 1;
|
|
}
|
|
|
|
// Verify a data block against its checksum record. Tree nodes carry their
|
|
// checksum in the parent pointer; data blocks have no parent, so the check must
|
|
// be an explicit lookup. Returns 0 if good, -2 if it fails, and 0 as well when
|
|
// no record exists — an image may legitimately predate checksums for a block,
|
|
// and refusing to read data over a *missing* checksum would turn an upgrade
|
|
// into a total loss.
|
|
static int
|
|
csum_check(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dblk,
|
|
const void *data)
|
|
{
|
|
struct schfs_key k;
|
|
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
|
|
|
|
uint32_t want;
|
|
unsigned len = sizeof(want);
|
|
cur_bind(c, v);
|
|
int r = schfs_btree_lookup_cur(c, &k, &want, &len);
|
|
if (r == -1)
|
|
return 0;
|
|
if (r != 0 || len != sizeof(want))
|
|
return -2;
|
|
|
|
if (schfs_crc32c(0, data, SCHFS_BLKSIZE) != want)
|
|
return -2;
|
|
return 0;
|
|
}
|
|
|
|
long
|
|
schfs_file_read(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
|
|
uint64_t off, void *buf, unsigned n, void *dblk)
|
|
{
|
|
/* schfs_inode_get caches for itself now, so this is a hit on every chunk
|
|
* after the first without a second cache here to keep coherent. */
|
|
struct schfs_inode in;
|
|
int r = schfs_inode_get(v, c, ino, &in);
|
|
if (r != 0)
|
|
return r;
|
|
|
|
if (off >= in.size)
|
|
return 0;
|
|
if (off + n > in.size)
|
|
n = (unsigned)(in.size - off);
|
|
|
|
char *dst = (char *)buf;
|
|
unsigned done = 0;
|
|
while (done < n) {
|
|
uint64_t fblk = (off + done) / SCHFS_BLKSIZE;
|
|
unsigned inblk = (unsigned)((off + done) % SCHFS_BLKSIZE);
|
|
unsigned want = SCHFS_BLKSIZE - inblk;
|
|
if (want > n - done)
|
|
want = n - done;
|
|
|
|
uint64_t disk = 0;
|
|
int m = extent_map(v, c, ino, fblk, &disk);
|
|
if (m == -2)
|
|
return -2;
|
|
|
|
if (m == 0) {
|
|
memset(dst + done, 0, want); /* hole reads as zeros */
|
|
} else {
|
|
if (v->dev.bread(v->dev.ctx, disk, dblk) != 0)
|
|
return -2;
|
|
if (csum_check(v, c, disk, dblk) != 0)
|
|
return -2;
|
|
memmove(dst + done, (char *)dblk + inblk, want);
|
|
}
|
|
done += want;
|
|
}
|
|
return (long)done;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Path resolution */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
uint64_t
|
|
schfs_path_lookup(struct schfs_vol *v, struct schfs_cursor *c, const char *path)
|
|
{
|
|
uint64_t cur = v->root_objectid;
|
|
|
|
while (*path == '/')
|
|
path++;
|
|
|
|
while (*path) {
|
|
const char *s = path;
|
|
while (*path && *path != '/')
|
|
path++;
|
|
unsigned len = (unsigned)(path - s);
|
|
while (*path == '/')
|
|
path++;
|
|
|
|
if (len == 1 && s[0] == '.')
|
|
continue;
|
|
if (len == 2 && s[0] == '.' && s[1] == '.') {
|
|
// ".." is synthetic: directories store a parent id rather than an
|
|
// entry, so walking up is an inode read, not a lookup.
|
|
struct schfs_inode in;
|
|
if (schfs_inode_get(v, c, cur, &in) != 0)
|
|
return 0;
|
|
cur = in.parent ? in.parent : v->root_objectid;
|
|
continue;
|
|
}
|
|
|
|
// Every component but the last must be a directory. Checking here is
|
|
// what makes "cat /etc/passwd/x" fail rather than silently succeed.
|
|
struct schfs_inode in;
|
|
if (schfs_inode_get(v, c, cur, &in) != 0)
|
|
return 0;
|
|
if (in.type != SCHFS_T_DIR)
|
|
return 0;
|
|
|
|
uint64_t next;
|
|
if (schfs_dir_lookup(v, c, cur, s, len, &next, 0) != 0)
|
|
return 0;
|
|
cur = next;
|
|
}
|
|
return cur;
|
|
}
|