Initial commit: Schunix as it stands before the SchOS rename
An x86-64 operating system built from xv6, now largely rewritten: SMP, a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF rasteriser, and a Linux-numbered syscall ABI. No history before this point -- the tree was never under version control. This commit exists so the rename that follows has something to undo to.
This commit is contained in:
commit
d0f7acf9eb
275 changed files with 51417 additions and 0 deletions
932
kernel/fs/schfs/objw.c
Normal file
932
kernel/fs/schfs/objw.c
Normal file
|
|
@ -0,0 +1,932 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// SchFS object layer, write side: create, link, unlink, rename, write,
|
||||
// truncate.
|
||||
//
|
||||
// The same portable C as the read side, compiled both hosted (mkschfs) and
|
||||
// freestanding (the kernel mount), so an image the tools produce and one the
|
||||
// kernel produces come from a single implementation rather than two that agree
|
||||
// by luck. Everything below is expressed as records in the one B-tree; there is
|
||||
// no inode table, no directory block format and no bitmap to keep in step.
|
||||
//
|
||||
// Two rules run through the whole file.
|
||||
//
|
||||
// Nothing here may put a block on the stack. A SchFS block is 4096 bytes and so
|
||||
// is the entire kernel stack, so every function borrows the volume's three
|
||||
// scratch buffers (v->ctx) instead. Where a cursor is needed, ctx.a doubles as
|
||||
// its buffer — which is safe only because no path below holds a cursor across a
|
||||
// tree mutation, and every one of them copies what it needs out first.
|
||||
//
|
||||
// Nothing reachable from a committed root is ever overwritten. For tree blocks
|
||||
// the B-tree enforces that; for *data* blocks it is enforced here, by
|
||||
// schfs_extent.gen. A run allocated during the group being built may be written
|
||||
// in place, because no published root can see it. Anything older is copied to a
|
||||
// fresh block and the extent record split around it.
|
||||
|
||||
#include "schfs_int.h"
|
||||
|
||||
#ifdef SCHFS_HOSTED
|
||||
#include <string.h>
|
||||
#else
|
||||
#include "types.h"
|
||||
#include "defs.h"
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Inodes */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
uint64_t
|
||||
schfs_alloc_objectid(struct schfs_vol *v)
|
||||
{
|
||||
return v->next_objectid++;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_inode_put(struct schfs_vol *v, uint64_t ino, const struct schfs_inode *in)
|
||||
{
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
|
||||
return schfs_btree_insert(&v->tree, &v->ctx, &k, in, sizeof(*in));
|
||||
}
|
||||
|
||||
// Read an inode using the volume's own scratch block. Every mutation below is a
|
||||
// read-modify-write of one of these, and each one that took a cursor buffer
|
||||
// from its caller was one more place to get the aliasing rules wrong.
|
||||
static int
|
||||
inode_load(struct schfs_vol *v, uint64_t ino, struct schfs_inode *out)
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &v->tree, v->ctx.a);
|
||||
return schfs_inode_get(v, &c, ino, out);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Directory entries */
|
||||
/* */
|
||||
/* One key per name hash, holding a packed list of records, because two */
|
||||
/* names in a directory can hash the same and the second insert would */
|
||||
/* otherwise silently replace the first. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
// Load the packed list for `name`'s hash into v->dirbuf. Returns its length, or
|
||||
// 0 if there is no such key, or a negative error.
|
||||
static int
|
||||
dirlist_load(struct schfs_vol *v, uint64_t dir, const char *name,
|
||||
unsigned namelen, struct schfs_key *k)
|
||||
{
|
||||
schfs_mkkey(k, dir, SCHFS_KEY_DIRENT, schfs_namehash(name, namelen));
|
||||
|
||||
unsigned len = sizeof(v->dirbuf);
|
||||
int r = schfs_btree_lookup(&v->tree, &v->ctx, k, v->dirbuf, &len);
|
||||
if (r == -2)
|
||||
return -2;
|
||||
if (r != 0)
|
||||
return 0;
|
||||
if (len > sizeof(v->dirbuf))
|
||||
return -2; /* stored value larger than can exist */
|
||||
return (int)len;
|
||||
}
|
||||
|
||||
// Offset of `name` within the packed list, or -1 if absent, -2 if malformed.
|
||||
static int
|
||||
dirlist_find(const char *list, unsigned len, const char *name, unsigned namelen)
|
||||
{
|
||||
for (unsigned off = 0; off + sizeof(struct schfs_dirent_rec) <= len; ) {
|
||||
const struct schfs_dirent_rec *d =
|
||||
(const struct schfs_dirent_rec *)(list + off);
|
||||
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
|
||||
if (step == 0 || off + step > len)
|
||||
return -2;
|
||||
if (d->namelen == namelen &&
|
||||
memcmp(list + off + sizeof(*d), name, namelen) == 0)
|
||||
return (int)off;
|
||||
off += step;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_dir_add(struct schfs_vol *v, uint64_t dir, const char *name,
|
||||
unsigned namelen, uint64_t child, uint16_t type)
|
||||
{
|
||||
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
|
||||
return -1;
|
||||
|
||||
struct schfs_key k;
|
||||
int got = dirlist_load(v, dir, name, namelen, &k);
|
||||
if (got < 0)
|
||||
return got;
|
||||
unsigned len = (unsigned)got;
|
||||
|
||||
// Reject a duplicate outright. Without this, adding a name twice produces a
|
||||
// directory with two entries that lookup can never tell apart.
|
||||
int at = dirlist_find(v->dirbuf, len, name, namelen);
|
||||
if (at == -2)
|
||||
return -2;
|
||||
if (at >= 0)
|
||||
return -1;
|
||||
|
||||
unsigned add = SCHFS_DIRENT_LEN(namelen);
|
||||
if (len + add > sizeof(v->dirbuf))
|
||||
return -4; /* collision list will not fit in one item */
|
||||
|
||||
struct schfs_dirent_rec *d = (struct schfs_dirent_rec *)(v->dirbuf + len);
|
||||
memset(d, 0, add);
|
||||
d->objectid = child;
|
||||
d->type = type;
|
||||
d->namelen = (uint16_t)namelen;
|
||||
memmove(v->dirbuf + len + sizeof(*d), name, namelen);
|
||||
|
||||
return schfs_btree_insert(&v->tree, &v->ctx, &k, v->dirbuf, len + add);
|
||||
}
|
||||
|
||||
int
|
||||
schfs_dir_del(struct schfs_vol *v, uint64_t dir, const char *name,
|
||||
unsigned namelen)
|
||||
{
|
||||
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
|
||||
return -1;
|
||||
|
||||
struct schfs_key k;
|
||||
int got = dirlist_load(v, dir, name, namelen, &k);
|
||||
if (got < 0)
|
||||
return got;
|
||||
unsigned len = (unsigned)got;
|
||||
if (len == 0)
|
||||
return -1;
|
||||
|
||||
int at = dirlist_find(v->dirbuf, len, name, namelen);
|
||||
if (at == -2)
|
||||
return -2;
|
||||
if (at < 0)
|
||||
return -1;
|
||||
|
||||
const struct schfs_dirent_rec *d =
|
||||
(const struct schfs_dirent_rec *)(v->dirbuf + at);
|
||||
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
|
||||
memmove(v->dirbuf + at, v->dirbuf + at + step, len - (unsigned)at - step);
|
||||
len -= step;
|
||||
|
||||
// An emptied list means the key itself goes, not a zero-length value: a key
|
||||
// that exists with nothing in it would make the directory scan stop on a
|
||||
// record readdir cannot describe.
|
||||
if (len == 0)
|
||||
return schfs_btree_delete(&v->tree, &v->ctx, &k);
|
||||
return schfs_btree_insert(&v->tree, &v->ctx, &k, v->dirbuf, len);
|
||||
}
|
||||
|
||||
int
|
||||
schfs_dir_empty(struct schfs_vol *v, uint64_t dir)
|
||||
{
|
||||
struct schfs_key start;
|
||||
schfs_mkkey(&start, dir, SCHFS_KEY_DIRENT, 0);
|
||||
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &v->tree, v->ctx.a);
|
||||
int r = schfs_cursor_seek(&c, &start);
|
||||
if (r == -2)
|
||||
return -2;
|
||||
if (r != 0)
|
||||
return 1; /* nothing at or after: empty */
|
||||
|
||||
const struct schfs_key *got = schfs_cursor_key(&c);
|
||||
if (!got)
|
||||
return 1;
|
||||
return (got->objectid == dir && got->type == SCHFS_KEY_DIRENT) ? 0 : 1;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Data-block checksums */
|
||||
/* */
|
||||
/* Tree nodes carry their checksum in their parent's pointer. Data */
|
||||
/* blocks have no parent, so theirs live under one reserved object id, */
|
||||
/* exactly as btrfs keeps a checksum tree. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static int
|
||||
csum_put(struct schfs_vol *v, uint64_t dblk, const void *data)
|
||||
{
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
|
||||
uint32_t c = schfs_crc32c(0, data, SCHFS_BLKSIZE);
|
||||
return schfs_btree_insert(&v->tree, &v->ctx, &k, &c, sizeof(c));
|
||||
}
|
||||
|
||||
static int
|
||||
csum_del(struct schfs_vol *v, uint64_t dblk)
|
||||
{
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
|
||||
int r = schfs_btree_delete(&v->tree, &v->ctx, &k);
|
||||
return r == -1 ? 0 : r; /* absent is not a failure */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Extents */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static int
|
||||
extent_put(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
|
||||
const struct schfs_extent *e)
|
||||
{
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
|
||||
return schfs_btree_insert(&v->tree, &v->ctx, &k, e, sizeof(*e));
|
||||
}
|
||||
|
||||
static int
|
||||
extent_del(struct schfs_vol *v, uint64_t ino, uint64_t fblk)
|
||||
{
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
|
||||
int r = schfs_btree_delete(&v->tree, &v->ctx, &k);
|
||||
return r == -1 ? 0 : r;
|
||||
}
|
||||
|
||||
// The extent covering file block `fblk`, if any. 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). Returns 1 and fills *start and *e, 0 for a hole, -2
|
||||
// on corruption.
|
||||
static int
|
||||
extent_find(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
|
||||
uint64_t *start, struct schfs_extent *e)
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &v->tree, v->ctx.a);
|
||||
|
||||
struct schfs_key probe;
|
||||
schfs_mkkey(&probe, ino, SCHFS_KEY_EXTENT, fblk);
|
||||
int r = schfs_btree_lookup_le(&c, &probe);
|
||||
if (r == -2)
|
||||
return -2;
|
||||
if (r != 0)
|
||||
return 0;
|
||||
|
||||
const struct schfs_key *got = schfs_cursor_key(&c);
|
||||
unsigned vlen;
|
||||
const struct schfs_extent *found =
|
||||
(const struct schfs_extent *)schfs_cursor_val(&c, &vlen);
|
||||
if (!got || !found)
|
||||
return 0;
|
||||
// lookup_le happily lands on some other object's records; so does a sparse
|
||||
// file whose first extent starts past this block.
|
||||
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
|
||||
return 0;
|
||||
if (vlen != sizeof(*e))
|
||||
return -2;
|
||||
if (fblk >= got->offset + found->nblocks)
|
||||
return 0;
|
||||
|
||||
*start = got->offset;
|
||||
*e = *found;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The last extent record belonging to `ino`, or 0 if it has none.
|
||||
static int
|
||||
extent_last(struct schfs_vol *v, uint64_t ino, uint64_t *start,
|
||||
struct schfs_extent *e)
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &v->tree, v->ctx.a);
|
||||
|
||||
struct schfs_key probe;
|
||||
schfs_mkkey(&probe, ino, SCHFS_KEY_EXTENT, ~0ULL);
|
||||
int r = schfs_btree_lookup_le(&c, &probe);
|
||||
if (r == -2)
|
||||
return -2;
|
||||
if (r != 0)
|
||||
return 0;
|
||||
|
||||
const struct schfs_key *got = schfs_cursor_key(&c);
|
||||
unsigned vlen;
|
||||
const struct schfs_extent *found =
|
||||
(const struct schfs_extent *)schfs_cursor_val(&c, &vlen);
|
||||
if (!got || !found)
|
||||
return 0;
|
||||
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
|
||||
return 0;
|
||||
if (vlen != sizeof(*e))
|
||||
return -2;
|
||||
|
||||
*start = got->offset;
|
||||
*e = *found;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Record a newly allocated single block, merging it into the preceding extent
|
||||
// when the two are adjacent both in the file and on the disk.
|
||||
//
|
||||
// This is what keeps extents worth having. Sequential writes get consecutive
|
||||
// blocks from the allocator, so without merging a one-megabyte file carries 256
|
||||
// extent records instead of one, and the tree ends up mostly describing where
|
||||
// the data is rather than holding it.
|
||||
static int
|
||||
extent_grow(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
|
||||
const struct schfs_extent *e)
|
||||
{
|
||||
if (fblk > 0) {
|
||||
uint64_t pstart;
|
||||
struct schfs_extent p;
|
||||
int m = extent_find(v, ino, fblk - 1, &pstart, &p);
|
||||
if (m == -2)
|
||||
return -2;
|
||||
if (m == 1 &&
|
||||
pstart + p.nblocks == fblk &&
|
||||
p.disk_block + p.nblocks == e->disk_block &&
|
||||
p.refs == e->refs && p.gen == e->gen) {
|
||||
p.nblocks++;
|
||||
return extent_put(v, ino, pstart, &p);
|
||||
}
|
||||
}
|
||||
return extent_put(v, ino, fblk, e);
|
||||
}
|
||||
|
||||
// Release a run of data blocks and the checksum records that describe them.
|
||||
// Shared runs are left alone: another file still points at them, and proper
|
||||
// refcounting needs an extent tree that nothing creates yet.
|
||||
static int
|
||||
extent_release(struct schfs_vol *v, const struct schfs_extent *e,
|
||||
uint64_t first, uint64_t n)
|
||||
{
|
||||
if (e->refs != 1)
|
||||
return 0;
|
||||
for (uint64_t i = 0; i < n; i++) {
|
||||
int r = csum_del(v, first + i);
|
||||
if (r != 0)
|
||||
return r;
|
||||
v->dev.bfree(v->dev.ctx, first + i);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Drop everything from file block `from` onward. Used by truncate and by the
|
||||
// deletion of an object.
|
||||
static int
|
||||
extents_drop_from(struct schfs_vol *v, uint64_t ino, struct schfs_inode *in,
|
||||
uint64_t from)
|
||||
{
|
||||
for (;;) {
|
||||
uint64_t start;
|
||||
struct schfs_extent e;
|
||||
int m = extent_last(v, ino, &start, &e);
|
||||
if (m == -2)
|
||||
return -2;
|
||||
if (m == 0)
|
||||
return 0; /* no extents left */
|
||||
if (start + e.nblocks <= from)
|
||||
return 0; /* nothing beyond the cut */
|
||||
|
||||
if (start >= from) {
|
||||
int r = extent_release(v, &e, e.disk_block, e.nblocks);
|
||||
if (r != 0)
|
||||
return r;
|
||||
r = extent_del(v, ino, start);
|
||||
if (r != 0)
|
||||
return r;
|
||||
if (in->blocks >= e.nblocks)
|
||||
in->blocks -= e.nblocks;
|
||||
continue; /* there may be earlier ones too */
|
||||
}
|
||||
|
||||
// The cut falls inside this run: keep its head, release its tail.
|
||||
uint64_t keep = from - start;
|
||||
uint64_t tail = e.nblocks - keep;
|
||||
int r = extent_release(v, &e, e.disk_block + keep, tail);
|
||||
if (r != 0)
|
||||
return r;
|
||||
e.nblocks = keep;
|
||||
r = extent_put(v, ino, start, &e);
|
||||
if (r != 0)
|
||||
return r;
|
||||
if (in->blocks >= tail)
|
||||
in->blocks -= tail;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* File data */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
int
|
||||
schfs_file_reserve(struct schfs_vol *v, uint64_t ino, uint64_t nblocks)
|
||||
{
|
||||
if (nblocks == 0)
|
||||
return 0;
|
||||
|
||||
// Take the whole run before touching the tree.
|
||||
//
|
||||
// This is what makes extents worth having when the final size is already
|
||||
// known. Allocating a data block, then inserting its extent record -- which
|
||||
// itself allocates blocks for the copy-on-write path -- then allocating the
|
||||
// next data block interleaves metadata with data, so consecutive file
|
||||
// blocks are never physically adjacent. Grabbing the run up front keeps it
|
||||
// contiguous and a whole file becomes one record.
|
||||
uint64_t first;
|
||||
if (v->dev.balloc_run) {
|
||||
first = v->dev.balloc_run(v->dev.ctx, nblocks);
|
||||
if (first == 0)
|
||||
return -3;
|
||||
} else {
|
||||
first = v->dev.balloc(v->dev.ctx);
|
||||
if (first == 0)
|
||||
return -3;
|
||||
uint64_t prev = first;
|
||||
for (uint64_t i = 1; i < nblocks; i++) {
|
||||
uint64_t b = v->dev.balloc(v->dev.ctx);
|
||||
if (b == 0)
|
||||
return -3;
|
||||
if (b != prev + 1)
|
||||
return -2; /* allocator is not handing out a run */
|
||||
prev = b;
|
||||
}
|
||||
}
|
||||
|
||||
struct schfs_extent e;
|
||||
memset(&e, 0, sizeof(e));
|
||||
e.disk_block = first;
|
||||
e.nblocks = nblocks;
|
||||
e.refs = 1;
|
||||
e.gen = v->tree.generation;
|
||||
|
||||
// Deliberately does not touch the inode. Re-reading and rewriting it here
|
||||
// costs two extra tree inserts per file, and every insert copies its whole
|
||||
// path. The caller knows the block count before it writes the inode the
|
||||
// first time, so it sets in.blocks there instead.
|
||||
return extent_put(v, ino, 0, &e);
|
||||
}
|
||||
|
||||
// Where the next write to file block `fblk` should land.
|
||||
struct wblk {
|
||||
uint64_t disk; // block to write
|
||||
uint64_t src; // block holding the current contents, 0 for a hole
|
||||
int fresh; // a block that did not exist before
|
||||
};
|
||||
|
||||
static int
|
||||
block_for_write(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
|
||||
struct wblk *w)
|
||||
{
|
||||
uint64_t start;
|
||||
struct schfs_extent e;
|
||||
int m = extent_find(v, ino, fblk, &start, &e);
|
||||
if (m == -2)
|
||||
return -2;
|
||||
|
||||
if (m == 0) {
|
||||
uint64_t d = v->dev.balloc(v->dev.ctx);
|
||||
if (d == 0)
|
||||
return -3;
|
||||
|
||||
struct schfs_extent ne;
|
||||
memset(&ne, 0, sizeof(ne));
|
||||
ne.disk_block = d;
|
||||
ne.nblocks = 1;
|
||||
ne.refs = 1;
|
||||
ne.gen = v->tree.generation;
|
||||
|
||||
int r = extent_grow(v, ino, fblk, &ne);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
w->disk = d;
|
||||
w->src = 0;
|
||||
w->fresh = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t old = e.disk_block + (fblk - start);
|
||||
|
||||
// Allocated by the group being built, and owned outright: no published root
|
||||
// can reach it, so it may simply be overwritten.
|
||||
if (e.gen == v->tree.generation && e.refs == 1) {
|
||||
w->disk = old;
|
||||
w->src = old;
|
||||
w->fresh = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Otherwise copy this one block out of the run, and split the record around
|
||||
// it: a head covering what came before, the new block, and a tail covering
|
||||
// what came after. No deletion is needed — when the block being replaced is
|
||||
// the run's first, the middle record simply overwrites the original key.
|
||||
uint64_t nd = v->dev.balloc(v->dev.ctx);
|
||||
if (nd == 0)
|
||||
return -3;
|
||||
|
||||
if (fblk > start) {
|
||||
struct schfs_extent head = e;
|
||||
head.nblocks = fblk - start;
|
||||
int r = extent_put(v, ino, start, &head);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
{
|
||||
struct schfs_extent mid;
|
||||
memset(&mid, 0, sizeof(mid));
|
||||
mid.disk_block = nd;
|
||||
mid.nblocks = 1;
|
||||
mid.refs = 1;
|
||||
mid.gen = v->tree.generation;
|
||||
int r = extent_put(v, ino, fblk, &mid);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
if (fblk + 1 < start + e.nblocks) {
|
||||
struct schfs_extent tail = e;
|
||||
tail.disk_block = e.disk_block + (fblk + 1 - start);
|
||||
tail.nblocks = start + e.nblocks - (fblk + 1);
|
||||
int r = extent_put(v, ino, fblk + 1, &tail);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
// The block just replaced rejoins the free map at the next commit, not now:
|
||||
// until then the committed tree still points at it and a crash lands there.
|
||||
int r = extent_release(v, &e, old, 1);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
w->disk = nd;
|
||||
w->src = old;
|
||||
w->fresh = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
long
|
||||
schfs_file_write(struct schfs_vol *v, uint64_t ino, uint64_t off,
|
||||
const void *buf, unsigned n, void *dblk)
|
||||
{
|
||||
struct schfs_inode in;
|
||||
int r = inode_load(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
const char *src = (const char *)buf;
|
||||
unsigned done = 0;
|
||||
|
||||
while (done < n) {
|
||||
uint64_t at = off + done;
|
||||
uint64_t fblk = at / SCHFS_BLKSIZE;
|
||||
unsigned inblk = (unsigned)(at % SCHFS_BLKSIZE);
|
||||
unsigned want = SCHFS_BLKSIZE - inblk;
|
||||
if (want > n - done)
|
||||
want = n - done;
|
||||
|
||||
struct wblk w;
|
||||
r = block_for_write(v, ino, fblk, &w);
|
||||
if (r != 0)
|
||||
return r;
|
||||
if (w.fresh)
|
||||
in.blocks++;
|
||||
|
||||
// A whole-block write needs no read: every byte is about to be
|
||||
// replaced. A partial one has to start from whatever is there, or from
|
||||
// zeros where the file had a hole.
|
||||
if (inblk != 0 || want != SCHFS_BLKSIZE) {
|
||||
if (w.src) {
|
||||
if (v->dev.bread(v->dev.ctx, w.src, dblk) != 0)
|
||||
return -2;
|
||||
} else {
|
||||
memset(dblk, 0, SCHFS_BLKSIZE);
|
||||
}
|
||||
}
|
||||
memmove((char *)dblk + inblk, src + done, want);
|
||||
|
||||
if (v->dev.bwrite(v->dev.ctx, w.disk, dblk) != 0)
|
||||
return -2;
|
||||
// The checksum is rewritten every time the block changes, not just when
|
||||
// it is allocated, or a topped-up tail block fails its own check on the
|
||||
// very next read.
|
||||
r = csum_put(v, w.disk, dblk);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
done += want;
|
||||
}
|
||||
|
||||
if (off + n > in.size)
|
||||
in.size = off + n;
|
||||
in.gen = v->tree.generation;
|
||||
r = schfs_inode_put(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
return (long)done;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_file_append(struct schfs_vol *v, uint64_t ino, const void *buf,
|
||||
unsigned n, void *dblk)
|
||||
{
|
||||
struct schfs_inode in;
|
||||
int r = inode_load(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r == -1 ? -2 : r;
|
||||
|
||||
long got = schfs_file_write(v, ino, in.size, buf, n, dblk);
|
||||
if (got < 0)
|
||||
return (int)got;
|
||||
return got == (long)n ? 0 : -2;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_file_truncate(struct schfs_vol *v, uint64_t ino, uint64_t size,
|
||||
void *dblk)
|
||||
{
|
||||
struct schfs_inode in;
|
||||
int r = inode_load(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
if (size < in.size) {
|
||||
uint64_t keep = (size + SCHFS_BLKSIZE - 1) / SCHFS_BLKSIZE;
|
||||
r = extents_drop_from(v, ino, &in, keep);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
// Whole blocks past the new end are gone; the block the new size falls
|
||||
// *inside* has to have its tail zeroed. It is tempting not to bother,
|
||||
// since a read stops at in.size and can never see those bytes — but
|
||||
// truncating down and then growing again is required to read back
|
||||
// zeros, and without this it reads back whatever the file used to hold.
|
||||
unsigned tail = (unsigned)(size % SCHFS_BLKSIZE);
|
||||
if (tail != 0) {
|
||||
uint64_t fblk = size / SCHFS_BLKSIZE;
|
||||
uint64_t start;
|
||||
struct schfs_extent e;
|
||||
int m = extent_find(v, ino, fblk, &start, &e);
|
||||
if (m == -2)
|
||||
return -2;
|
||||
if (m == 1) { /* a hole is already zeros */
|
||||
struct wblk w;
|
||||
r = block_for_write(v, ino, fblk, &w);
|
||||
if (r != 0)
|
||||
return r;
|
||||
if (w.src) {
|
||||
if (v->dev.bread(v->dev.ctx, w.src, dblk) != 0)
|
||||
return -2;
|
||||
} else {
|
||||
memset(dblk, 0, SCHFS_BLKSIZE);
|
||||
}
|
||||
memset((char *)dblk + tail, 0, SCHFS_BLKSIZE - tail);
|
||||
if (v->dev.bwrite(v->dev.ctx, w.disk, dblk) != 0)
|
||||
return -2;
|
||||
r = csum_put(v, w.disk, dblk);
|
||||
if (r != 0)
|
||||
return r;
|
||||
if (w.fresh)
|
||||
in.blocks++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Growing needs nothing at all: the range with no extent records reads back
|
||||
// as zeros, which is a sparse file for free.
|
||||
|
||||
in.size = size;
|
||||
in.gen = v->tree.generation;
|
||||
return schfs_inode_put(v, ino, &in);
|
||||
}
|
||||
|
||||
// Release an object entirely: its data, its checksums and its inode record.
|
||||
int
|
||||
schfs_object_free(struct schfs_vol *v, uint64_t ino)
|
||||
{
|
||||
struct schfs_inode in;
|
||||
int r = inode_load(v, ino, &in);
|
||||
if (r == -1)
|
||||
return 0; /* already gone */
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
r = extents_drop_from(v, ino, &in, 0);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
|
||||
r = schfs_btree_delete(&v->tree, &v->ctx, &k);
|
||||
return r == -1 ? 0 : r;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Namespace */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
// A directory's link count is 2 (itself and its own ".") plus one per
|
||||
// subdirectory, because each subdirectory's ".." points back at it. Neither
|
||||
// entry is stored, but the count still has to match what POSIX tools expect or
|
||||
// "find -type d" prunes real directories and du walks the tree wrongly.
|
||||
static int
|
||||
parent_nlink_adjust(struct schfs_vol *v, uint64_t dir, int delta)
|
||||
{
|
||||
struct schfs_inode in;
|
||||
int r = inode_load(v, dir, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
if (delta < 0 && in.nlink < (uint32_t)(-delta))
|
||||
return -2;
|
||||
in.nlink = (uint32_t)((int64_t)in.nlink + delta);
|
||||
return schfs_inode_put(v, dir, &in);
|
||||
}
|
||||
|
||||
int
|
||||
schfs_create(struct schfs_vol *v, uint64_t dir, const char *name,
|
||||
unsigned namelen, uint16_t type, uint32_t mode,
|
||||
uint16_t rdev_major, uint16_t rdev_minor, uint64_t now,
|
||||
uint64_t *out_ino)
|
||||
{
|
||||
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
|
||||
return -1;
|
||||
|
||||
struct schfs_inode parent;
|
||||
int r = inode_load(v, dir, &parent);
|
||||
if (r != 0)
|
||||
return r;
|
||||
if (parent.type != SCHFS_T_DIR)
|
||||
return -1;
|
||||
|
||||
uint64_t ino = schfs_alloc_objectid(v);
|
||||
|
||||
struct schfs_inode in;
|
||||
memset(&in, 0, sizeof(in));
|
||||
in.type = type;
|
||||
in.parent = dir;
|
||||
in.mode = mode;
|
||||
in.nlink = type == SCHFS_T_DIR ? 2 : 1;
|
||||
in.rdev_major = rdev_major;
|
||||
in.rdev_minor = rdev_minor;
|
||||
in.atime = in.mtime = in.ctime = now;
|
||||
in.gen = v->tree.generation;
|
||||
|
||||
r = schfs_inode_put(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
// The entry goes in last, so a failure leaves an unreferenced object rather
|
||||
// than a name pointing at nothing. Neither survives a crash — none of this
|
||||
// is visible until the transaction commits — but an unreachable inode is
|
||||
// the harmless one of the two if a later step in the same group fails.
|
||||
r = schfs_dir_add(v, dir, name, namelen, ino, type);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
if (type == SCHFS_T_DIR) {
|
||||
r = parent_nlink_adjust(v, dir, +1);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (out_ino)
|
||||
*out_ino = ino;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_link(struct schfs_vol *v, uint64_t dir, const char *name,
|
||||
unsigned namelen, uint64_t ino)
|
||||
{
|
||||
struct schfs_inode in;
|
||||
int r = inode_load(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
// Directories are not hard-linkable. Allowing it makes cycles possible, and
|
||||
// a cycle turns every recursive walk into an infinite one.
|
||||
if (in.type == SCHFS_T_DIR)
|
||||
return -1;
|
||||
|
||||
r = schfs_dir_add(v, dir, name, namelen, ino, in.type);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
in.nlink++;
|
||||
in.ctime = in.mtime;
|
||||
return schfs_inode_put(v, ino, &in);
|
||||
}
|
||||
|
||||
int
|
||||
schfs_unlink(struct schfs_vol *v, uint64_t dir, const char *name,
|
||||
unsigned namelen)
|
||||
{
|
||||
uint64_t ino;
|
||||
uint16_t type;
|
||||
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &v->tree, v->ctx.a);
|
||||
int r = schfs_dir_lookup(v, &c, dir, name, namelen, &ino, &type);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
struct schfs_inode in;
|
||||
int r = inode_load(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
if (in.type == SCHFS_T_DIR) {
|
||||
int e = schfs_dir_empty(v, ino);
|
||||
if (e == -2)
|
||||
return -2;
|
||||
if (e == 0)
|
||||
return -5; /* directory not empty */
|
||||
}
|
||||
|
||||
r = schfs_dir_del(v, dir, name, namelen);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
if (in.type == SCHFS_T_DIR) {
|
||||
r = parent_nlink_adjust(v, dir, -1);
|
||||
if (r != 0)
|
||||
return r;
|
||||
return schfs_object_free(v, ino);
|
||||
}
|
||||
|
||||
if (in.nlink > 1) {
|
||||
in.nlink--;
|
||||
return schfs_inode_put(v, ino, &in);
|
||||
}
|
||||
return schfs_object_free(v, ino);
|
||||
}
|
||||
|
||||
int
|
||||
schfs_rename(struct schfs_vol *v, uint64_t olddir, const char *oldname,
|
||||
unsigned oldlen, uint64_t newdir, const char *newname,
|
||||
unsigned newlen)
|
||||
{
|
||||
uint64_t ino;
|
||||
uint16_t type;
|
||||
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &v->tree, v->ctx.a);
|
||||
int r = schfs_dir_lookup(v, &c, olddir, oldname, oldlen, &ino, &type);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
// Renaming a directory into its own subtree would detach that subtree from
|
||||
// the root and leave it reachable only from itself. Walking up from the new
|
||||
// parent is the cheap way to catch it, since every directory records its
|
||||
// own parent.
|
||||
if (type == SCHFS_T_DIR && newdir != olddir) {
|
||||
uint64_t up = newdir;
|
||||
for (int i = 0; i <= SCHFS_MAX_LEVEL * 8 && up != 0; i++) {
|
||||
if (up == ino)
|
||||
return -1;
|
||||
struct schfs_inode t;
|
||||
if (inode_load(v, up, &t) != 0)
|
||||
break;
|
||||
if (up == v->root_objectid)
|
||||
break;
|
||||
up = t.parent;
|
||||
}
|
||||
}
|
||||
|
||||
// A target that already exists is replaced, which means removing it first —
|
||||
// and refusing when that would silently discard a non-empty directory.
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &v->tree, v->ctx.a);
|
||||
uint64_t victim;
|
||||
if (schfs_dir_lookup(v, &c, newdir, newname, newlen, &victim, 0) == 0) {
|
||||
if (victim == ino)
|
||||
return 0; /* renaming a name onto itself */
|
||||
int r = schfs_unlink(v, newdir, newname, newlen);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
int r = schfs_dir_add(v, newdir, newname, newlen, ino, type);
|
||||
if (r != 0)
|
||||
return r;
|
||||
r = schfs_dir_del(v, olddir, oldname, oldlen);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
if (newdir != olddir) {
|
||||
if (type == SCHFS_T_DIR) {
|
||||
// ".." is synthesised from the inode's parent field, so moving a
|
||||
// directory means rewriting that field and moving the link its ".."
|
||||
// contributes to each parent's count.
|
||||
struct schfs_inode in;
|
||||
r = inode_load(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
in.parent = newdir;
|
||||
r = schfs_inode_put(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
r = parent_nlink_adjust(v, olddir, -1);
|
||||
if (r != 0)
|
||||
return r;
|
||||
r = parent_nlink_adjust(v, newdir, +1);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in a new issue