feat(cubelinux-2): Package 3 — cubefs (POSIX/FUSE namespace over CZYX)
Implements the PDF's Package 3 with new code:
- path: bijective POSIX path <-> Czyx mapping (/c001/z002/y003/x004).
Axis-letter + 3-digit zero-padded canonical names so lexical order equals
numeric order and each coordinate has exactly one spelling. Inode IS the
packed u32 coordinate — no inode side table.
- nullspace: the PDF's 'use Null cubes for ACLs, xattrs, journaling, volume
metadata', with the Z-plane allocation fixed and documented (Z=1 volume,
Z=2 ACL, Z=3 xattr, Z=4 journal ring). ACL/xattr tables are FNV
hash-bucketed with exact-match resolution inside the bucket, because 4
axes of subject cannot injectively mirror into 2 axes of Null space.
Journal is a bounded ring; wraps are detectable via a monotonic counter.
- vfs: the whole filesystem, kernel-free and unit-testable — lookup,
readdir, create/read/write/truncate/unlink, mkdir/rmdir, ACL enforcement,
xattrs, journaling, POSIX errno mapping.
- fuse (feature 'mount'): thin kernel adapter, zero TTL (the store is
writable out-of-band, so cached metadata would go stale).
- cubestore: added the PDF's 'optional scanning primitives' (keys,
scan_prefix) and the Package 2 association API (associate, linked_to)
that cubefs needs for directory listings.
Two defects were found by LIVE MOUNT testing and fixed, not by unit tests:
1. mkdir succeeded then the kernel's revalidating lookup returned ENOENT,
so 'mkdir -p' could never reach depth 4. Directories were purely
inferred from records, making an empty directory unrepresentable. Fixed
with an explicit Null-space directory marker; rmdir removes it; readdir
merges markers in. 5 regression tests added.
2. Multi-user ACL behaviour was untestable because the mount lacked
AllowOther — the kernel returned EACCES at the mountpoint before any
request reached us. Added --allow-other.
Verified: 58 unit tests pass; clippy clean; live mount exercised with cat,
echo, dd, truncate, cp, chmod, chown, getfattr/setfattr, mkdir -p, rmdir,
find, a 200-record write loop, and cross-user reads/writes as luulu.
This commit is contained in:
@@ -28,6 +28,34 @@ pub trait CubeBackend {
|
||||
fn get(&self, key: &Czyx) -> Option<Vec<u8>>;
|
||||
/// Remove the value at `key`.
|
||||
fn delete(&mut self, key: &Czyx);
|
||||
|
||||
/// Optional scanning primitive (PDF Package 2: "plus optional scanning
|
||||
/// primitives"). Returns every coordinate currently present.
|
||||
///
|
||||
/// Decision: this is a provided method returning an empty `Vec` by
|
||||
/// default so existing backends stay source-compatible, and so a backend
|
||||
/// that cannot enumerate cheaply (a remote/blind KV) can honestly report
|
||||
/// "no enumeration" instead of lying. `cubefs` needs enumeration to build
|
||||
/// directory listings, and documents that requirement at its own API.
|
||||
fn keys(&self) -> Vec<Czyx> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Coordinates whose `C` (and optionally `Z`, `Y`) prefix matches.
|
||||
///
|
||||
/// Provided in terms of [`CubeBackend::keys`]; a real on-disk backend
|
||||
/// should override this with a range scan over the packed `u32` key,
|
||||
/// which is prefix-ordered because `pack_u32` puts `C` in the high byte.
|
||||
fn scan_prefix(&self, c: u8, z: Option<u8>, y: Option<u8>) -> Vec<Czyx> {
|
||||
self.keys()
|
||||
.into_iter()
|
||||
.filter(|k| {
|
||||
k.c == c
|
||||
&& z.map(|zz| k.z == zz).unwrap_or(true)
|
||||
&& y.map(|yy| k.y == yy).unwrap_or(true)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// In-memory backend backed by a `HashMap<u32, Vec<u8>>` keyed by the packed
|
||||
@@ -61,6 +89,13 @@ impl CubeBackend for HashBackend {
|
||||
fn delete(&mut self, key: &Czyx) {
|
||||
self.0.remove(&key.pack_u32());
|
||||
}
|
||||
fn keys(&self) -> Vec<Czyx> {
|
||||
let mut v: Vec<Czyx> = self.0.keys().map(|k| Czyx::unpack_u32(*k)).collect();
|
||||
// Deterministic order: HashMap iteration is unordered, but callers
|
||||
// (cubefs readdir) need a stable listing.
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
/// A record store: a header + body addressed by a [`Czyx`] label.
|
||||
@@ -314,6 +349,55 @@ impl<B: CubeBackend> CubeStore<B> {
|
||||
pub fn delete_raw(&mut self, key: &Czyx) {
|
||||
self.backend.delete(key);
|
||||
}
|
||||
|
||||
/// Every coordinate present in the backend (requires a backend that
|
||||
/// implements [`CubeBackend::keys`]).
|
||||
pub fn keys(&self) -> Vec<Czyx> {
|
||||
self.backend.keys()
|
||||
}
|
||||
|
||||
/// Coordinates under a `C`/`Z`/`Y` prefix.
|
||||
pub fn scan_prefix(&self, c: u8, z: Option<u8>, y: Option<u8>) -> Vec<Czyx> {
|
||||
self.backend.scan_prefix(c, z, y)
|
||||
}
|
||||
|
||||
/// PDF Package 2 API: link `src` to `dst` by appending `dst` to `src`'s
|
||||
/// `linked_records` and refreshing the association flag.
|
||||
///
|
||||
/// Decision: the association is stored one-way in the source header (as
|
||||
/// the PDF's "association flags" describe), and reverse lookup is done by
|
||||
/// scanning (see [`CubeStore::linked_to`]). Storing a reverse index would
|
||||
/// double-write every association and risk divergence; scanning is cheap
|
||||
/// against the packed-u32 key space and always consistent.
|
||||
/// Returns `false` if `src` does not exist.
|
||||
pub fn associate(&mut self, src: Czyx, dst: Czyx) -> bool {
|
||||
let Some((mut h, body)) = self.get_record(&src) else {
|
||||
return false;
|
||||
};
|
||||
if !h.linked_records.contains(&dst) {
|
||||
h.linked_records.push(dst);
|
||||
}
|
||||
h.refresh_flags();
|
||||
self.put_record(src, &h, &body);
|
||||
true
|
||||
}
|
||||
|
||||
/// PDF Package 2 API: "all records linked to X" — every coordinate whose
|
||||
/// header lists `target` in its `linked_records`.
|
||||
pub fn linked_to(&self, target: &Czyx) -> Vec<Czyx> {
|
||||
let mut out: Vec<Czyx> = self
|
||||
.backend
|
||||
.keys()
|
||||
.into_iter()
|
||||
.filter(|k| {
|
||||
self.get_record(k)
|
||||
.map(|(h, _)| h.linked_records.contains(target))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect();
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user