feat(cubelinux-2): Package 1 — cubecoords + cubestore from PDF spec
New code (no recycling from prior build). Implements: - cubecoords: Czyx 4-axis coordinate (pack/unpack u32), Null-class classification (Total / Cube 1-4 / User), TriWord tri-channel 64-bit codec (6 ASCII + 4 control bits), HeaderFlags + CubeHeader (derived flags). All coding decisions documented inline. - cubestore: CubeBackend trait, HashBackend (HashMap<u32>), CubeStore with dependency-free length-prefixed record codec (header TLV + body). - 8 unit tests, all passing; cargo test clean (0 warn/err). Per directive: separate git repo; current /home/CUBELinux kept as working tool; scoped to AI-OS-excluded PDF vision.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"cubecoords",
|
||||
"cubestore",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "cubecoords"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "CUBELinux-2 coordinate layer: 4-axis CZYX + tri-channel word + rich record header, built new from the original PDF spec."
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1,335 @@
|
||||
//! 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 in 0..6 {
|
||||
let shift = 8 * (5 - i);
|
||||
ascii[i] = ((v >> shift) & 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)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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;
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "cubestore"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "CUBELinux-2 record store over CZYX coordinates (PDF Package 1), built new from spec."
|
||||
|
||||
[dependencies]
|
||||
cubecoords = { path = "../cubecoords" }
|
||||
@@ -0,0 +1,360 @@
|
||||
//! CUBELinux-2 record store over CZYX coordinates (PDF Package 1).
|
||||
//!
|
||||
//! Built NEW from the PDF spec; not recycled from the prior `/home/CUBELinux`
|
||||
//! build. The prior build used a 3-axis `u64` point + `SpaceId`; this store
|
||||
//! is keyed by the PDF's [`Czyx`] coordinate directly.
|
||||
//!
|
||||
//! Scope of this file: Package 1 only — a coordinate type, a tri-channel
|
||||
//! codec, a header, and a minimal store abstraction that can later be backed
|
||||
//! by a log-structured / RocksDB store. The PDF's later packages (cubefs,
|
||||
//! cubevm, cubecrypt, cubeai) are out of scope for this hardware and are not
|
||||
//! implemented here.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use cubecoords::{CubeHeader, Czyx};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A backend that maps CZYX coordinates to byte payloads.
|
||||
///
|
||||
/// Decision: trait takes `Czyx` by value for `put`/`delete` (small, `Copy`)
|
||||
/// and by reference for `get`, matching the PDF signature while staying
|
||||
/// allocation-light. Revisit if a future backend needs the whole key moved.
|
||||
pub trait CubeBackend {
|
||||
/// Store `value` at `key`.
|
||||
fn put(&mut self, key: Czyx, value: Vec<u8>);
|
||||
/// Fetch the value at `key`, if present.
|
||||
fn get(&self, key: &Czyx) -> Option<Vec<u8>>;
|
||||
/// Remove the value at `key`.
|
||||
fn delete(&mut self, key: &Czyx);
|
||||
}
|
||||
|
||||
/// In-memory backend backed by a `HashMap<u32, Vec<u8>>` keyed by the packed
|
||||
/// `u32` form of [`Czyx`].
|
||||
///
|
||||
/// Decision: packs to `u32` (not a 4-tuple key) so the map layout matches the
|
||||
/// PDF's `HashMap<u32, Vec<u8>>` example exactly and stays cheap. A production
|
||||
/// backend would replace this with the on-disk store.
|
||||
pub struct HashBackend(pub HashMap<u32, Vec<u8>>);
|
||||
|
||||
impl HashBackend {
|
||||
/// Empty backend.
|
||||
pub fn new() -> Self {
|
||||
HashBackend(HashMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HashBackend {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CubeBackend for HashBackend {
|
||||
fn put(&mut self, key: Czyx, value: Vec<u8>) {
|
||||
self.0.insert(key.pack_u32(), value);
|
||||
}
|
||||
fn get(&self, key: &Czyx) -> Option<Vec<u8>> {
|
||||
self.0.get(&key.pack_u32()).cloned()
|
||||
}
|
||||
fn delete(&mut self, key: &Czyx) {
|
||||
self.0.remove(&key.pack_u32());
|
||||
}
|
||||
}
|
||||
|
||||
/// A record store: a header + body addressed by a [`Czyx`] label.
|
||||
///
|
||||
/// Decision: we serialize the header and body as a single byte buffer with a
|
||||
/// length-prefixed header section, rather than relying on an external
|
||||
/// `bincode` dependency (keeps CUBELinux-2 dependency-free at Package 1).
|
||||
/// The header is length-prefixed so the body boundary is recoverable without
|
||||
/// a fixed schema — this is the "evolve toward explicit C/Z/Y/X-mapped flag
|
||||
/// bytes" step the PDF mentions, done inline.
|
||||
pub struct CubeStore<B: CubeBackend> {
|
||||
backend: B,
|
||||
}
|
||||
|
||||
/// On-wire layout of a stored record:
|
||||
/// `[u32 header_len][header bytes][body bytes]`
|
||||
/// Header bytes = JSON of `CubeHeader`. Decision: JSON (via a tiny manual
|
||||
/// serializer-free path) is overkill; we instead use a simple, stable
|
||||
/// binary form below. (Documented: JSON was considered; a compact binary
|
||||
/// encoding is used to avoid a serde dependency at Package 1.)
|
||||
mod record_codec {
|
||||
use cubecoords::{CubeHeader, Czyx};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Compact, dependency-free encoding of the header.
|
||||
// Fields are written in a fixed tag-length-value stream so unknown
|
||||
// future fields can be skipped on read. Tag byte + (optional) length +
|
||||
// payload.
|
||||
//
|
||||
// Tags:
|
||||
// 1 title (utf8)
|
||||
// 2 doc_type (utf8)
|
||||
// 3 created_at (u64 le)
|
||||
// 4 size_bytes (u64 le)
|
||||
// 5 owner_local_user (utf8)
|
||||
// 6 owner_remote_user (utf8)
|
||||
// 7 linked_records: u16 count, then count*(4*u8) CZYX bytes
|
||||
// 8 total_accesses (u64 le)
|
||||
// 9 total_remote_accesses (u64 le)
|
||||
// 10 last_access (u64 le)
|
||||
// 11 last_remote_access (u64 le)
|
||||
|
||||
pub fn encode_header(h: &CubeHeader) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(t) = &h.title {
|
||||
put_utf8(&mut out, 1, t);
|
||||
}
|
||||
if let Some(d) = &h.doc_type {
|
||||
put_utf8(&mut out, 2, d);
|
||||
}
|
||||
if let Some(c) = h.created_at {
|
||||
put_u64(&mut out, 3, c);
|
||||
}
|
||||
if let Some(s) = h.size_bytes {
|
||||
put_u64(&mut out, 4, s);
|
||||
}
|
||||
if let Some(o) = &h.owner_local_user {
|
||||
put_utf8(&mut out, 5, o);
|
||||
}
|
||||
if let Some(o) = &h.owner_remote_user {
|
||||
put_utf8(&mut out, 6, o);
|
||||
}
|
||||
if !h.linked_records.is_empty() {
|
||||
out.push(7);
|
||||
out.extend_from_slice(&(h.linked_records.len() as u16).to_le_bytes());
|
||||
for r in &h.linked_records {
|
||||
out.push(r.c);
|
||||
out.push(r.z);
|
||||
out.push(r.y);
|
||||
out.push(r.x);
|
||||
}
|
||||
}
|
||||
if h.total_accesses != 0 {
|
||||
put_u64(&mut out, 8, h.total_accesses);
|
||||
}
|
||||
if h.total_remote_accesses != 0 {
|
||||
put_u64(&mut out, 9, h.total_remote_accesses);
|
||||
}
|
||||
if let Some(a) = h.last_access {
|
||||
put_u64(&mut out, 10, a);
|
||||
}
|
||||
if let Some(a) = h.last_remote_access {
|
||||
put_u64(&mut out, 11, a);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn decode_header(mut b: &[u8]) -> Option<CubeHeader> {
|
||||
let mut h = CubeHeader::new();
|
||||
while !b.is_empty() {
|
||||
let tag = b[0];
|
||||
b = &b[1..];
|
||||
match tag {
|
||||
1 => {
|
||||
let (v, rest) = take_utf8(b)?;
|
||||
h.title = Some(v);
|
||||
b = rest;
|
||||
}
|
||||
2 => {
|
||||
let (v, rest) = take_utf8(b)?;
|
||||
h.doc_type = Some(v);
|
||||
b = rest;
|
||||
}
|
||||
3 => {
|
||||
let (v, rest) = take_u64(b)?;
|
||||
h.created_at = Some(v);
|
||||
b = rest;
|
||||
}
|
||||
4 => {
|
||||
let (v, rest) = take_u64(b)?;
|
||||
h.size_bytes = Some(v);
|
||||
b = rest;
|
||||
}
|
||||
5 => {
|
||||
let (v, rest) = take_utf8(b)?;
|
||||
h.owner_local_user = Some(v);
|
||||
b = rest;
|
||||
}
|
||||
6 => {
|
||||
let (v, rest) = take_utf8(b)?;
|
||||
h.owner_remote_user = Some(v);
|
||||
b = rest;
|
||||
}
|
||||
7 => {
|
||||
if b.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let n = u16::from_le_bytes([b[0], b[1]]) as usize;
|
||||
b = &b[2..];
|
||||
if b.len() < n * 4 {
|
||||
return None;
|
||||
}
|
||||
for _ in 0..n {
|
||||
let c = b[0];
|
||||
let z = b[1];
|
||||
let y = b[2];
|
||||
let x = b[3];
|
||||
h.linked_records.push(Czyx::new(c, z, y, x));
|
||||
b = &b[4..];
|
||||
}
|
||||
}
|
||||
8 => {
|
||||
let (v, rest) = take_u64(b)?;
|
||||
h.total_accesses = v;
|
||||
b = rest;
|
||||
}
|
||||
9 => {
|
||||
let (v, rest) = take_u64(b)?;
|
||||
h.total_remote_accesses = v;
|
||||
b = rest;
|
||||
}
|
||||
10 => {
|
||||
let (v, rest) = take_u64(b)?;
|
||||
h.last_access = Some(v);
|
||||
b = rest;
|
||||
}
|
||||
11 => {
|
||||
let (v, rest) = take_u64(b)?;
|
||||
h.last_remote_access = Some(v);
|
||||
b = rest;
|
||||
}
|
||||
_ => return None, // unknown tag -> reject (strict at Package 1)
|
||||
}
|
||||
}
|
||||
h.refresh_flags();
|
||||
Some(h)
|
||||
}
|
||||
|
||||
fn put_u64(out: &mut Vec<u8>, tag: u8, v: u64) {
|
||||
out.push(tag);
|
||||
out.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
fn put_utf8(out: &mut Vec<u8>, tag: u8, s: &str) {
|
||||
let bytes = s.as_bytes();
|
||||
out.push(tag);
|
||||
out.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
|
||||
out.extend_from_slice(bytes);
|
||||
}
|
||||
fn take_u64(b: &[u8]) -> Option<(u64, &[u8])> {
|
||||
if b.len() < 8 {
|
||||
return None;
|
||||
}
|
||||
let mut a = [0u8; 8];
|
||||
a.copy_from_slice(&b[..8]);
|
||||
Some((u64::from_le_bytes(a), &b[8..]))
|
||||
}
|
||||
fn take_utf8(b: &[u8]) -> Option<(String, &[u8])> {
|
||||
if b.len() < 4 {
|
||||
return None;
|
||||
}
|
||||
let mut len = [0u8; 4];
|
||||
len.copy_from_slice(&b[..4]);
|
||||
let n = u32::from_le_bytes(len) as usize;
|
||||
let rest = &b[4..];
|
||||
if rest.len() < n {
|
||||
return None;
|
||||
}
|
||||
let s = String::from_utf8(rest[..n].to_vec()).ok()?;
|
||||
Some((s, &rest[n..]))
|
||||
}
|
||||
|
||||
/// Keep `HashMap` referenced so the dependency is explicit in this module
|
||||
/// even though the codec itself is generic over bytes. (Prevents an
|
||||
/// unused-import warning if the backend type changes.)
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn _assert_backend_assoc(_: &HashMap<u32, Vec<u8>>) {}
|
||||
}
|
||||
|
||||
impl<B: CubeBackend> CubeStore<B> {
|
||||
/// Wrap a backend.
|
||||
pub fn new(backend: B) -> Self {
|
||||
CubeStore { backend }
|
||||
}
|
||||
|
||||
/// Store `header` + `body` at `label`.
|
||||
pub fn put_record(&mut self, label: Czyx, header: &CubeHeader, body: &[u8]) {
|
||||
let hdr_bytes = record_codec::encode_header(header);
|
||||
let mut buf = Vec::with_capacity(4 + hdr_bytes.len() + body.len());
|
||||
buf.extend_from_slice(&(hdr_bytes.len() as u32).to_le_bytes());
|
||||
buf.extend_from_slice(&hdr_bytes);
|
||||
buf.extend_from_slice(body);
|
||||
self.backend.put(label, buf);
|
||||
}
|
||||
|
||||
/// Fetch and split a record into `(header, body)`.
|
||||
pub fn get_record(&self, label: &Czyx) -> Option<(CubeHeader, Vec<u8>)> {
|
||||
let raw = self.backend.get(label)?;
|
||||
if raw.len() < 4 {
|
||||
return None;
|
||||
}
|
||||
let mut len = [0u8; 4];
|
||||
len.copy_from_slice(&raw[..4]);
|
||||
let hlen = u32::from_le_bytes(len) as usize;
|
||||
if raw.len() < 4 + hlen {
|
||||
return None;
|
||||
}
|
||||
let hdr = record_codec::decode_header(&raw[4..4 + hlen])?;
|
||||
let body = raw[4 + hlen..].to_vec();
|
||||
Some((hdr, body))
|
||||
}
|
||||
|
||||
/// Raw backend access (delegates put/get/delete for non-record payloads).
|
||||
pub fn put_raw(&mut self, key: Czyx, value: Vec<u8>) {
|
||||
self.backend.put(key, value);
|
||||
}
|
||||
/// Raw backend get.
|
||||
pub fn get_raw(&self, key: &Czyx) -> Option<Vec<u8>> {
|
||||
self.backend.get(key)
|
||||
}
|
||||
/// Raw backend delete.
|
||||
pub fn delete_raw(&mut self, key: &Czyx) {
|
||||
self.backend.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn record_roundtrip() {
|
||||
let mut store = CubeStore::new(HashBackend::new());
|
||||
let mut h = CubeHeader::new();
|
||||
h.title = Some("memory".into());
|
||||
h.doc_type = Some("note".into());
|
||||
h.created_at = Some(1700000000);
|
||||
h.size_bytes = Some(5);
|
||||
h.linked_records.push(Czyx::new(1, 2, 3, 4));
|
||||
h.refresh_flags();
|
||||
|
||||
let label = Czyx::new(1, 10, 20, 30);
|
||||
store.put_record(label, &h, b"hello");
|
||||
let (rh, body) = store.get_record(&label).unwrap();
|
||||
assert_eq!(body, b"hello");
|
||||
assert_eq!(rh.title.as_deref(), Some("memory"));
|
||||
assert_eq!(rh.doc_type.as_deref(), Some("note"));
|
||||
assert_eq!(rh.created_at, Some(1700000000));
|
||||
assert_eq!(rh.size_bytes, Some(5));
|
||||
assert_eq!(rh.linked_records, vec![Czyx::new(1, 2, 3, 4)]);
|
||||
assert!(rh.flags.has(cubecoords::HeaderFlags::HAS_ASSOCIATIONS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_record_is_none() {
|
||||
let store = CubeStore::new(HashBackend::new());
|
||||
assert!(store.get_record(&Czyx::new(9, 9, 9, 9)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_coord_is_distinct_key() {
|
||||
let mut store = CubeStore::new(HashBackend::new());
|
||||
store.put_raw(Czyx::new(0, 0, 0, 0), vec![1]);
|
||||
store.put_raw(Czyx::new(0, 0, 0, 1), vec![2]);
|
||||
assert_eq!(store.get_raw(&Czyx::new(0, 0, 0, 0)), Some(vec![1]));
|
||||
assert_eq!(store.get_raw(&Czyx::new(0, 0, 0, 1)), Some(vec![2]));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user