//! 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, /// Flag 2: document type (like a file extension). pub doc_type: Option, /// Flag 3: creation time (epoch seconds). pub created_at: Option, /// Flag 4: payload size in bytes. pub size_bytes: Option, /// Owner: local user. pub owner_local_user: Option, /// Owner: remote user. pub owner_remote_user: Option, /// Association links to other records (flags 5–19). pub linked_records: Vec, /// 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, /// Timestamp of the last remote access, if any. pub last_remote_access: Option, } 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)); } }