Files
cubelinux-2/cubecoords/src/lib.rs
T
CUBELinux-2 35e5183193 feat(system): bind cubefs+cubecode+cubecrypt into one running system (cubesys)
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).
2026-08-10 23:42:38 -04:00

349 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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));
}
}