Integrates Packages 3-5 over a single shared CubeStore, the literal CUBELinux premise (data addressed by coordinate, not path). Adds the cubesys crate (lib + cube CLI + cube-demo) proving two end-to-end properties: a cubefs path IS a runnable code cell at the same coordinate, and a sealed record reopens and runs on the same store. Two latent cross-crate bugs surfaced and fixed while integrating: - cubecoords: refresh_flags() now preserves out-of-band flag bits (8..=15), so cubecrypt's HEADER_FLAG_ENCRYPTED survives refresh. - cubestore: record codec now serializes raw flag bits (TLV tag 12) so the encrypted bit survives the store round-trip. All gates green (./check, incl. cubefs --features mount).
349 lines
12 KiB
Rust
349 lines
12 KiB
Rust
//! CUBELinux-2 coordinate layer — built NEW from the original PDF spec.
|
||
//!
|
||
//! This is NOT recycled from the prior `/home/CUBELinux` build. The prior
|
||
//! build collapsed the PDF's four-axis CZYX model into three `u64` spatial
|
||
//! axes plus a 256-bit `SpaceId` capability. Here we restore the PDF model
|
||
//! faithfully: a 4×`u8` `CZYX` coordinate where the `C` axis (class/context)
|
||
//! is a real coordinate axis, a reserved "Null" control space for headers and
|
||
//! flags, and a 64-bit "tri-channel" word that packs six ASCII characters
|
||
//! plus four control bits.
|
||
//!
|
||
//! Coding decisions encountered while building to spec are documented inline
|
||
//! (see `Decision:` notes) so the divergence from a naive reading is visible.
|
||
|
||
#![forbid(unsafe_code)]
|
||
#![warn(missing_docs)]
|
||
|
||
/// A coordinate in the four-axis CUBELinux space.
|
||
///
|
||
/// Axes are each `u8` (0–255). The value `0` is reserved as "Null" on every
|
||
/// axis, following the PDF's design: `0` is not a normal data cell, it is the
|
||
/// control plane.
|
||
///
|
||
/// User record space is `1..=255` on each axis, giving
|
||
/// `255^4 = 4,228,250,625` possible record coordinates — the figure quoted in
|
||
/// the 2006 notes and the PDF.
|
||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||
pub struct Czyx {
|
||
/// Class / context axis. `0` = Null (control space).
|
||
pub c: u8,
|
||
/// Z axis (depth).
|
||
pub z: u8,
|
||
/// Y axis (vertical).
|
||
pub y: u8,
|
||
/// X axis (horizontal).
|
||
pub x: u8,
|
||
}
|
||
|
||
impl Czyx {
|
||
/// Construct a coordinate.
|
||
#[inline]
|
||
pub const fn new(c: u8, z: u8, y: u8, x: u8) -> Self {
|
||
Czyx { c, z, y, x }
|
||
}
|
||
|
||
/// Total Null: `C=Z=Y=X=0`. Used as end-of-record / unused / deletion
|
||
/// marker per the PDF.
|
||
#[inline]
|
||
pub fn is_total_null(&self) -> bool {
|
||
self.c == 0 && self.z == 0 && self.y == 0 && self.x == 0
|
||
}
|
||
|
||
/// Pack into a single `u32` with `C` in the high byte, `X` in the low byte.
|
||
#[inline]
|
||
pub fn pack_u32(&self) -> u32 {
|
||
((self.c as u32) << 24) | ((self.z as u32) << 16) | ((self.y as u32) << 8) | (self.x as u32)
|
||
}
|
||
|
||
/// Unpack a `u32` produced by [`pack_u32`].
|
||
#[inline]
|
||
pub fn unpack_u32(v: u32) -> Self {
|
||
Czyx {
|
||
c: (v >> 24) as u8,
|
||
z: (v >> 16) as u8,
|
||
y: (v >> 8) as u8,
|
||
x: v as u8,
|
||
}
|
||
}
|
||
|
||
// --- Null-class classification ---------------------------------------
|
||
//
|
||
// Decision: the PDF describes several Null special ranges ("Null cube 1–4,
|
||
// header layers, null rows") without a single canonical enumeration. We
|
||
// implement the two it defines precisely (Total Null, and the
|
||
// `C=0, Z/Y/X in 1..=255` Null-cube family) and expose a `NullClass` enum
|
||
// for the rest to be added as the store grows. This keeps the axis model
|
||
// exact while leaving a documented extension point.
|
||
|
||
/// Returns true if this coordinate lives in the Null control space:
|
||
/// `C == 0` with at least one of Z/Y/X non-zero (the "Null cube" family).
|
||
#[inline]
|
||
pub fn is_null_cube(&self) -> bool {
|
||
self.c == 0 && (self.z != 0 || self.y != 0 || self.x != 0)
|
||
}
|
||
|
||
/// Classify this coordinate.
|
||
#[inline]
|
||
pub fn null_class(&self) -> NullClass {
|
||
if self.is_total_null() {
|
||
NullClass::Total
|
||
} else if self.is_null_cube() {
|
||
// The PDF names "Null cube 1–4" by different Z/Y/X patterns.
|
||
// Decision: encode the cube number as a function of which
|
||
// non-zero pattern is present, deterministically, so it is
|
||
// stable and documented rather than ad hoc.
|
||
match (self.z != 0, self.y != 0, self.x != 0) {
|
||
(true, _, _) => NullClass::Cube(1),
|
||
(false, true, _) => NullClass::Cube(2),
|
||
(false, false, true) => NullClass::Cube(3),
|
||
(false, false, false) => NullClass::Cube(4), // unreachable after Total check, kept total
|
||
}
|
||
} else {
|
||
NullClass::User
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The classification of a coordinate with respect to the Null control plane.
|
||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||
pub enum NullClass {
|
||
/// `C=Z=Y=X=0` — end-of-record / unused / deletion marker.
|
||
Total,
|
||
/// `C=0`, at least one of Z/Y/X non-zero — a Null control cube, numbered
|
||
/// `1..=4` by which axes are set (see [`Czyx::null_class`]).
|
||
Cube(u8),
|
||
/// Normal user data cell (`C >= 1`, or `C=0` only when used as plain data
|
||
/// outside the Null convention).
|
||
User,
|
||
}
|
||
|
||
/// A 64-bit "tri-channel" word.
|
||
///
|
||
/// Per the PDF, one word carries either three ASCII pairs plus 4 control bits
|
||
/// (6 ASCII chars), or alternate pair/triad/quad arrangements. We implement
|
||
/// the canonical `pack_6` / `unpack_6` form from the spec (6 ASCII bytes +
|
||
/// 4 control bits) and leave the pair/triad/quad specialization as a
|
||
/// documented extension point.
|
||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
|
||
pub struct TriWord(pub u64);
|
||
|
||
/// Stateless encoder/decoder for [`TriWord`].
|
||
pub struct TriEnc;
|
||
|
||
impl TriEnc {
|
||
/// Pack 6 ASCII bytes (0–255) plus 4 control bits (0–15) into one 64-bit
|
||
/// word.
|
||
///
|
||
/// Layout (high → low): `[4 control bits][48 ascii bits][12 unused]`.
|
||
/// The control bits occupy bits 60..=63; the six ASCII bytes occupy bits
|
||
/// 8..=59 (byte `i` at `8*(5-i)`), leaving the low 8 bits spare for future
|
||
/// use.
|
||
#[inline]
|
||
pub fn pack_6(control: u8, ascii: [u8; 6]) -> TriWord {
|
||
let mut v: u64 = 0;
|
||
v |= (control as u64 & 0x0F) << 60;
|
||
for (i, b) in ascii.iter().enumerate() {
|
||
let shift = 8 * (5 - i);
|
||
v |= (*b as u64) << shift;
|
||
}
|
||
TriWord(v)
|
||
}
|
||
|
||
/// Unpack a word produced by [`pack_6`].
|
||
#[inline]
|
||
pub fn unpack_6(word: TriWord) -> (u8, [u8; 6]) {
|
||
let v = word.0;
|
||
let control = ((v >> 60) & 0x0F) as u8;
|
||
let mut ascii = [0u8; 6];
|
||
for (i, b) in ascii.iter_mut().enumerate() {
|
||
*b = ((v >> (8 * (5 - i))) & 0xFF) as u8;
|
||
}
|
||
(control, ascii)
|
||
}
|
||
}
|
||
|
||
/// Header flag bits, mirroring the PDF's title/type/date/size/permission
|
||
/// flag layout (flags 1–4 explicitly; 5–19 reserved for permissions and
|
||
/// associations).
|
||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
|
||
pub struct HeaderFlags(pub u16);
|
||
|
||
impl HeaderFlags {
|
||
/// Flag 1: title start present.
|
||
pub const TITLE: u16 = 1 << 0;
|
||
/// Flag 2: document type present.
|
||
pub const DOC_TYPE: u16 = 1 << 1;
|
||
/// Flag 3: creation date present.
|
||
pub const CREATED_AT: u16 = 1 << 2;
|
||
/// Flag 4: weight/size present.
|
||
pub const SIZE_BYTES: u16 = 1 << 3;
|
||
/// Flag 5: root-only permission.
|
||
pub const PERM_ROOT_ONLY: u16 = 1 << 4;
|
||
/// Flag 6: local-user owner.
|
||
pub const PERM_LOCAL_USER: u16 = 1 << 5;
|
||
/// Flag 7: remote-user owner.
|
||
pub const PERM_REMOTE_USER: u16 = 1 << 6;
|
||
/// Flag 8: has outgoing association links.
|
||
pub const HAS_ASSOCIATIONS: u16 = 1 << 7;
|
||
// Flag 255 (conceptual end-of-header) is represented out-of-band by the
|
||
// record serializer; there is no bit for it.
|
||
|
||
/// Construct from a raw bitmask.
|
||
#[inline]
|
||
pub const fn from_bits(bits: u16) -> Self {
|
||
HeaderFlags(bits)
|
||
}
|
||
|
||
/// Alias for [`from_bits`] used by record decoders that reconstruct a
|
||
/// header's raw flag field.
|
||
#[inline]
|
||
pub const fn flags_from_bits(bits: u16) -> Self {
|
||
HeaderFlags(bits)
|
||
}
|
||
|
||
/// The raw bitmask.
|
||
#[inline]
|
||
pub const fn bits(&self) -> u16 {
|
||
self.0
|
||
}
|
||
|
||
/// Set a flag bit.
|
||
#[inline]
|
||
pub fn set(&mut self, flag: u16) {
|
||
self.0 |= flag;
|
||
}
|
||
|
||
/// Test a flag bit.
|
||
#[inline]
|
||
pub fn has(&self, flag: u16) -> bool {
|
||
self.0 & flag != 0
|
||
}
|
||
}
|
||
|
||
/// A record header, mirroring the PDF's title/type/date/perms/association
|
||
/// model.
|
||
///
|
||
/// Decision: the PDF gives both a `bitflags`-style `HeaderFlags` and a
|
||
/// structured `CubeHeader` with `Option` fields. We keep the structured form
|
||
/// (it is what the store serializes) and derive the flag bits from which
|
||
/// fields are `Some`. This avoids storing redundant flag+field data.
|
||
#[derive(Clone, Debug, Default)]
|
||
pub struct CubeHeader {
|
||
/// Flag bits (derived; kept in sync by the accessors).
|
||
pub flags: HeaderFlags,
|
||
/// Flag 1: human title.
|
||
pub title: Option<String>,
|
||
/// Flag 2: document type (like a file extension).
|
||
pub doc_type: Option<String>,
|
||
/// Flag 3: creation time (epoch seconds).
|
||
pub created_at: Option<u64>,
|
||
/// Flag 4: payload size in bytes.
|
||
pub size_bytes: Option<u64>,
|
||
/// Owner: local user.
|
||
pub owner_local_user: Option<String>,
|
||
/// Owner: remote user.
|
||
pub owner_remote_user: Option<String>,
|
||
/// Association links to other records (flags 5–19).
|
||
pub linked_records: Vec<Czyx>,
|
||
/// Total local accesses (from the PDF's association/permission flags).
|
||
pub total_accesses: u64,
|
||
/// Total remote accesses.
|
||
pub total_remote_accesses: u64,
|
||
/// Timestamp of the last local access, if any.
|
||
pub last_access: Option<u64>,
|
||
/// Timestamp of the last remote access, if any.
|
||
pub last_remote_access: Option<u64>,
|
||
}
|
||
|
||
impl CubeHeader {
|
||
/// Build a header, computing [`flags`] from the populated fields.
|
||
pub fn new() -> Self {
|
||
CubeHeader::default()
|
||
}
|
||
|
||
/// Recompute the flag bits from which fields are present. Call after
|
||
/// mutating fields so `flags` stays consistent with the structure.
|
||
///
|
||
/// Known field flags occupy bits 0–7 (title..associations). Any
|
||
/// out-of-band/spare bits set directly on `flags` (e.g.
|
||
/// `cubecrypt::HEADER_FLAG_ENCRYPTED` at bit 12) are preserved, because
|
||
/// they are not derived from structured fields and would otherwise be
|
||
/// clobbered on every refresh.
|
||
pub fn refresh_flags(&mut self) {
|
||
let mut f = 0u16;
|
||
if self.title.is_some() {
|
||
f |= HeaderFlags::TITLE;
|
||
}
|
||
if self.doc_type.is_some() {
|
||
f |= HeaderFlags::DOC_TYPE;
|
||
}
|
||
if self.created_at.is_some() {
|
||
f |= HeaderFlags::CREATED_AT;
|
||
}
|
||
if self.size_bytes.is_some() {
|
||
f |= HeaderFlags::SIZE_BYTES;
|
||
}
|
||
if self.owner_local_user.is_some() {
|
||
f |= HeaderFlags::PERM_LOCAL_USER;
|
||
}
|
||
if self.owner_remote_user.is_some() {
|
||
f |= HeaderFlags::PERM_REMOTE_USER;
|
||
}
|
||
if !self.linked_records.is_empty() {
|
||
f |= HeaderFlags::HAS_ASSOCIATIONS;
|
||
}
|
||
// Preserve spare/out-of-band flag bits (bits 8..=15) that are not
|
||
// derived from structured fields.
|
||
const DERIVED_BITS: u16 = 0x00FF;
|
||
f |= self.flags.bits() & !DERIVED_BITS;
|
||
self.flags = HeaderFlags::from_bits(f);
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn czyx_pack_roundtrips() {
|
||
let c = Czyx::new(12, 34, 56, 78);
|
||
assert_eq!(Czyx::unpack_u32(c.pack_u32()), c);
|
||
assert_eq!(c.pack_u32(), 0x0C_22_38_4E);
|
||
}
|
||
|
||
#[test]
|
||
fn total_null_detected() {
|
||
assert!(Czyx::new(0, 0, 0, 0).is_total_null());
|
||
assert!(!Czyx::new(0, 1, 0, 0).is_total_null());
|
||
}
|
||
|
||
#[test]
|
||
fn null_classification() {
|
||
assert_eq!(Czyx::new(0, 0, 0, 0).null_class(), NullClass::Total);
|
||
assert_eq!(Czyx::new(0, 9, 0, 0).null_class(), NullClass::Cube(1));
|
||
assert_eq!(Czyx::new(0, 0, 9, 0).null_class(), NullClass::Cube(2));
|
||
assert_eq!(Czyx::new(0, 0, 0, 9).null_class(), NullClass::Cube(3));
|
||
assert_eq!(Czyx::new(3, 0, 0, 0).null_class(), NullClass::User);
|
||
}
|
||
|
||
#[test]
|
||
fn triword_roundtrips() {
|
||
let ascii = *b"cubeln"; // 6 ascii bytes
|
||
let w = TriEnc::pack_6(0xA, ascii);
|
||
let (ctrl, back) = TriEnc::unpack_6(w);
|
||
assert_eq!(ctrl, 0xA);
|
||
assert_eq!(back, ascii);
|
||
}
|
||
|
||
#[test]
|
||
fn header_flags_derived() {
|
||
let mut h = CubeHeader::new();
|
||
h.title = Some("hello".into());
|
||
h.size_bytes = Some(42);
|
||
h.refresh_flags();
|
||
assert!(h.flags.has(HeaderFlags::TITLE));
|
||
assert!(h.flags.has(HeaderFlags::SIZE_BYTES));
|
||
assert!(!h.flags.has(HeaderFlags::DOC_TYPE));
|
||
}
|
||
}
|