diff --git a/Cargo.toml b/Cargo.toml index 443fc39..dc53001 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "cubefs", "cubecode", "cubecrypt", + "cubesys", ] [workspace.package] diff --git a/cubecoords/src/lib.rs b/cubecoords/src/lib.rs index 1220ce7..6de36d3 100644 --- a/cubecoords/src/lib.rs +++ b/cubecoords/src/lib.rs @@ -194,6 +194,13 @@ impl HeaderFlags { 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 { @@ -256,6 +263,12 @@ impl CubeHeader { /// 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() { @@ -279,6 +292,10 @@ impl CubeHeader { 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); } } diff --git a/cubecrypt/src/lib.rs b/cubecrypt/src/lib.rs index 8cd36bf..ccdb23e 100644 --- a/cubecrypt/src/lib.rs +++ b/cubecrypt/src/lib.rs @@ -38,22 +38,22 @@ pub use env::{CubeEnv, EnvError, Selector, HEADER_FLAG_ENCRYPTED}; pub use transform::{CryptoError, Key, KeySlot, TransformId}; use cubecoords::{CubeHeader, Czyx}; -use cubestore::{CubeStore, HashBackend}; +use cubestore::{CubeBackend, CubeStore}; /// An append-only access log living in a Null-cube range. Each [`AccessLog`] /// targets one Null-cube head coordinate and appends fixed-size entries /// (record coord + op byte + 8-byte timestamp). It is "tamper-evident" in the /// weak sense that the log itself can be stored encrypted via another /// [`CubeEnv`]; this type only provides the structure and append/read. -pub struct AccessLog { - store: CubeStore, +pub struct AccessLog { + store: CubeStore, head: Czyx, next: u8, } -impl AccessLog { +impl AccessLog { /// Bind a log to a Null-cube head coordinate (entries append along X). - pub fn new(store: CubeStore, head: Czyx) -> Self { + pub fn new(store: CubeStore, head: Czyx) -> Self { AccessLog { store, head, diff --git a/cubestore/src/lib.rs b/cubestore/src/lib.rs index 78edbc3..a64ed59 100644 --- a/cubestore/src/lib.rs +++ b/cubestore/src/lib.rs @@ -64,6 +64,7 @@ pub trait CubeBackend { /// Decision: packs to `u32` (not a 4-tuple key) so the map layout matches the /// PDF's `HashMap>` example exactly and stays cheap. A production /// backend would replace this with the on-disk store. +#[derive(Clone)] pub struct HashBackend(pub HashMap>); impl HashBackend { @@ -106,6 +107,7 @@ impl CubeBackend for HashBackend { /// 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. +#[derive(Clone)] pub struct CubeStore { backend: B, } @@ -180,6 +182,13 @@ mod record_codec { if let Some(a) = h.last_remote_access { put_u64(&mut out, 11, a); } + // Tag 12: raw flag bits. Serializes out-of-band/spare bits (e.g. + // `cubecrypt::HEADER_FLAG_ENCRYPTED`) that are not derived from + // structured fields, so they survive an encode/decode round-trip. + if h.flags.bits() != 0 { + out.push(12); + out.extend_from_slice(&h.flags.bits().to_le_bytes()); + } out } @@ -257,6 +266,14 @@ mod record_codec { h.last_remote_access = Some(v); b = rest; } + 12 => { + if b.len() < 2 { + return None; + } + let raw = u16::from_le_bytes([b[0], b[1]]); + h.flags = cubecoords::HeaderFlags::flags_from_bits(raw); + b = &b[2..]; + } _ => return None, // unknown tag -> reject (strict at Package 1) } } diff --git a/cubesys/Cargo.toml b/cubesys/Cargo.toml new file mode 100644 index 0000000..faaca47 --- /dev/null +++ b/cubesys/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "cubesys" +version = "0.1.0" +edition.workspace = true +license.workspace = true +description = "CUBELinux-2 system integration: one CubeStore shared by cubefs, cubecode and cubecrypt (PDF Packages 3-5 bound into a single running system)." + +[dependencies] +cubecoords = { path = "../cubecoords" } +cubestore = { path = "../cubestore" } +cubefs = { path = "../cubefs" } +cubecode = { path = "../cubecode" } +cubecrypt = { path = "../cubecrypt" } + +[[bin]] +name = "cube" +path = "src/bin/cube.rs" + +[[bin]] +name = "cube-demo" +path = "src/bin/cube_demo.rs" diff --git a/cubesys/docs/integration.md b/cubesys/docs/integration.md new file mode 100644 index 0000000..7acb470 --- /dev/null +++ b/cubesys/docs/integration.md @@ -0,0 +1,90 @@ +# CUBELinux-2 system integration (`cubesys`) + +This crate is the composition layer the PDF implies but does not name as a +"Package 6". It binds Packages 3-5 (cubefs, cubecode, cubecrypt) into one +running system over a **single shared `CubeStore`**, which is the literal +CUBELinux premise: *data addressed by coordinate, not path.* + +## The one invariant + +> The cube is the single source of truth. cubefs is a *view* of it, cubecode +> is a *kind* of record in it, cubecrypt is a *transform* applied to records +> in it. None of them own storage. + +That is what makes the integration real instead of three crates side by side: +every layer operates on the same backend, so a write through one is visible to +the others at the same coordinate. + +## What binds to what + +| Layer | Role in the system | Shares via | +|--------------|------------------------------------------------------|----------------------------------| +| `cubefs` | POSIX namespace; a path `C/Z/Y/X` is a coordinate | `CubeStore`, `path_to_czyx` | +| `cubecode` | code cells stored *as records*; VM runs them | `CubeStore`, `CodeCell` | +| `cubecrypt` | seals records under a `CubeEnv`; key in Null cubes | `CubeStore`, `CubeEnv` | + +Two primitives in this crate do the binding: + +* `path_to_czyx(path)` — the single bijection (re-exported from `cubefs`) so + path and coordinate never disagree. +* `load_code_cell(store, path)` — reads the record at a path's coordinate and + decodes it as cubevm bytecode. A cubefs file **is** a runnable VM cell. + +## End-to-end properties proved by the tests + +1. **A path is a function.** Writing bytecode at `/c012/z001/y001/x021` makes + cubefs list it as a file *and* the VM run it at `Czyx::new(12,1,1,21)` — + same coordinate, two views. (Note: an axis value of `0` is Null control + space and is rejected as a path component, so every user path maps to a + coordinate with all axes in `1..=255`; keys and metadata live in Null + space and are addressed directly by `C.Z.Y.X`.) +2. **Seal then run is the same record.** `CubeEnv::put_encrypted` at a + coordinate leaves the cube carrying an encrypted body (encrypted flag set); + the raw body no longer decodes as bytecode. To *execute* a sealed record the + system decrypts it back into a plaintext record at the same coordinate, then + runs the VM (the VM runs code located by coordinate; reading the envelope + directly would fail decode). `open` + `CodeCell::from_record` + `Vm::run` + recovers and executes it on the *same* store. + +## Bugs found and fixed while integrating (real, cross-crate) + +Building the integration turned up two latent cross-crate bugs that the +per-package test suites never exercised: + +* **`CubeHeader::refresh_flags()` dropped out-of-band flag bits.** It recomputed + `flags` only from structured fields (bits 0–7), so `cubecrypt`'s + `HEADER_FLAG_ENCRYPTED` (bit 12), which is set out-of-band, was wiped on every + refresh. `put_encrypted` calls `refresh_flags` before storing, so an encrypted + record lost its flag. Fixed by preserving spare bits (8..=15) in + `refresh_flags`. (`cubecoords`) +* **The record codec never serialized `flags`.** `record_codec::encode_header` + wrote only structured fields, and `decode_header` rebuilt `flags` from them — + so even a correctly-set encrypted bit was lost on the store round-trip. Added + tag `12` (raw flag bits) to the TLV codec so out-of-band bits survive. + (`cubestore`) + +Both are now guarded by `cubesys` integration tests. + +## Why this, and not a "Package 6" + +The PDF arc ends at cubecrypt; the only named next package is `cubeai`, which +the directive explicitly excluded (hardware cannot run it). Integration is the +honest completion: it turns five isolated crates into the coherent +coordinate-addressed system the design describes, without inventing a package +the spec never defined. + +## Binaries + +* `cube-demo` — self-contained tour (write two linked cells, run; seal a + record, reopen + run). Prints evidence at each step. +* `cube` — CLI: `write`, `run`, `ls`, `stat`, `seal`, `open` over one store, + interactively (`repl`), from a file (`script`), or as the demo (`demo`). + +## Building / testing + +```sh +cd /home/CUBELinux/CUBELinux-2 +./check quick # fmt + clippy -D warnings + tests (the gate) +./check mount # also builds cubefs --features mount (FUSE adapter) +cargo run -p cubesys --bin cube-demo +``` diff --git a/cubesys/src/bin/cube.rs b/cubesys/src/bin/cube.rs new file mode 100644 index 0000000..18c5976 --- /dev/null +++ b/cubesys/src/bin/cube.rs @@ -0,0 +1,311 @@ +//! `cube` — the CUBELinux-2 system CLI. +//! +//! One process holds ONE in-memory `CubeStore` shared by cubefs, cubecode and +//! cubecrypt. Commands are issued as a script (file) or interactively (REPL); +//! every command operates on that shared store, so `write` then `run` then +//! `seal` then `open` all see the same cube. +//! +//! Usage: +//! cube # print this help +//! cube demo # run the built-in integration demo +//! cube repl # read commands from stdin, one per line +//! cube script # read commands from , one per line +//! +//! Commands (operate on the shared cube): +//! write # store cubevm bytecode (hex/dec) at a path +//! run # load the code cell at and run the VM +//! ls # list a cubefs directory +//! stat # getattr via cubefs +//! seal # encrypt the record at (tf: none|gcm|chacha|xts) +//! open # decrypt + decode + run the sealed record +//! +//! Coordinates are written `C.Z.Y.X` (decimal). Key cells live in Null space, +//! so they are given directly as coordinates, not as cubefs paths. + +use std::io::BufRead; + +use cubecode::{CodeCell, Kind, Op, Vm}; +use cubecoords::CubeHeader; +use cubecrypt::{CubeEnv, KeySlot, Selector, TransformId}; +use cubestore::{CubeStore, HashBackend}; + +fn main() { + let args: Vec = std::env::args().collect(); + match args.get(1).map(|s| s.as_str()) { + None => print_help(), + Some("demo") => cubesys::demo::run(), + Some("repl") => { + let mut store = CubeStore::new(HashBackend::new()); + let stdin = std::io::stdin(); + let lock = stdin.lock(); + for line in lock.lines() { + let line = match line { + Ok(l) => l, + Err(_) => break, + }; + if line.trim().is_empty() || line.starts_with('#') { + continue; + } + match run_line(&mut store, &line) { + Ok(out) => println!("{out}"), + Err(e) => eprintln!("error: {e}"), + } + } + } + Some("script") => { + let file = args.get(2).expect("script needs "); + let text = std::fs::read_to_string(file).expect("cannot read script file"); + let mut store = CubeStore::new(HashBackend::new()); + for line in text.lines() { + if line.trim().is_empty() || line.starts_with('#') { + continue; + } + match run_line(&mut store, line) { + Ok(out) => println!("{out}"), + Err(e) => { + eprintln!("error: {e}"); + std::process::exit(1); + } + } + } + } + Some(other) => { + eprintln!("unknown subcommand: {other}\n"); + print_help(); + std::process::exit(2); + } + } +} + +fn print_help() { + println!( + "cube - CUBELinux-2 system CLI (cubefs + cubecode + cubecrypt over one store)\n\ + \n\ + Usage:\n \ + cube show this help\n \ + cube demo run the built-in integration demo\n \ + cube repl read commands from stdin (one per line)\n \ + cube script run commands from a file\n\ + \nCommands:\n \ + prog write CUBEVM bytecode from op names\n \ + write store cubevm bytecode at a path\n \ + run run the code cell at \n \ + ls list a cubefs directory\n \ + stat getattr via cubefs\n \ + seal encrypt a record (tf: none|gcm|chacha|xts)\n \ + open decrypt + decode + run a sealed record\n" + ); +} + +/// Execute one command line against the shared store. +fn run_line(store: &mut CubeStore, line: &str) -> Result { + let mut it = line.split_whitespace(); + let cmd = it.next().ok_or_else(|| "empty line".to_string())?; + match cmd { + "prog" => { + // prog — write CUBEVM bytecode assembled from + // op names (see `parse_op`). Example: + // prog /c005/z001/y001/x007 const 7 halt + let path = it.next().ok_or_else(|| "prog needs ".to_string())?; + let mut ops: Vec = Vec::new(); + while let Some(tok) = it.next() { + let arg = if takes_arg(tok) { + it.next() + .and_then(|a| a.parse::().ok()) + .ok_or_else(|| format!("prog: {tok} needs a u8 argument"))? + } else { + 0 + }; + ops.push(make_op(tok, arg)?); + } + if ops.is_empty() { + return Err("prog: no ops given".to_string()); + } + let name = path.rsplit('/').next().unwrap_or(path); + let coord = cubesys::store_code_cell(store, path, Kind::Fn, name, &[], &ops) + .map_err(|e| e.to_string())?; + Ok(format!( + "wrote program {path} -> coord {} ({} ops)", + coord.pack_u32(), + ops.len() + )) + } + "write" => { + let path = it.next().ok_or_else(|| "write needs ".to_string())?; + let bytes: Vec = it + .map(parse_byte) + .collect::>() + .ok_or_else(|| "write: every byte must be hex/dec 0..255".to_string())?; + let code = + cubecode::decode(&bytes).map_err(|e| format!("bytecode decode error: {e:?}"))?; + let name = path.rsplit('/').next().unwrap_or(path); + let coord = cubesys::store_code_cell(store, path, Kind::Fn, name, &[], &code) + .map_err(|e| e.to_string())?; + Ok(format!("wrote {path} -> coord {}", coord.pack_u32())) + } + "run" => { + let path = it.next().ok_or_else(|| "run needs ".to_string())?; + let cell = cubesys::load_code_cell(store, path).map_err(|e| e.to_string())?; + let mut vm = Vm::new(store.clone()); + let res = vm.run(cell.label); + let mut out = format!("run {path} => {res:?}"); + if !vm.output().is_empty() { + out.push_str(&format!( + "\n trace: {}", + String::from_utf8_lossy(vm.output()).trim_end() + )); + } + Ok(out) + } + "ls" => { + let dir = it.next().ok_or_else(|| "ls needs ".to_string())?; + let fs = cubefs::CubeFs::new(store.clone()); + let entries = fs.readdir(dir).map_err(|e| format!("ls {dir}: {e:?}"))?; + if entries.is_empty() { + Ok(format!("ls {dir} -> (empty)")) + } else { + let names: Vec = entries.into_iter().map(|(n, _)| n).collect(); + Ok(format!("ls {dir} -> {}", names.join(" "))) + } + } + "stat" => { + let path = it.next().ok_or_else(|| "stat needs ".to_string())?; + let fs = cubefs::CubeFs::new(store.clone()); + let a = fs + .getattr(path) + .map_err(|e| format!("stat {path}: {e:?}"))?; + Ok(format!( + "stat {path} -> ino={} kind={:?} size={} mode={:o}", + a.ino, a.kind, a.size, a.mode + )) + } + "seal" | "open" => { + let path = it.next().ok_or_else(|| format!("{cmd} needs "))?; + let keyc = it + .next() + .ok_or_else(|| format!("{cmd} needs key cell"))?; + let tf = it + .next() + .ok_or_else(|| format!("{cmd} needs "))?; + let coord = cubesys::path_to_czyx(path).map_err(|e| e.to_string())?; + let kc = + parse_coord(keyc).ok_or_else(|| "bad key-cell coord (use C.Z.Y.X)".to_string())?; + let transform = parse_transform(tf) + .ok_or_else(|| "unknown transform (none|gcm|chacha|xts)".to_string())?; + + // Ensure key material exists at the Null-cube key cell. + if store.get_record(&kc).is_none() { + store.put_record(kc, &CubeHeader::new(), b"demo-key-material-32-bytes-long!!"); + } + let env = CubeEnv::new( + vec![KeySlot { + key_cell: kc, + transform, + salt: vec![], + }], + vec![], + ); + + if cmd == "seal" { + let (h, body) = store + .get_record(&coord) + .ok_or_else(|| format!("seal: no record at {path}"))?; + env.put_encrypted(store, coord, Selector::Slot(0), &body, h) + .map_err(|e| format!("seal: {e:?}"))?; + Ok(format!("sealed {path} under key {} ({tf})", kc.pack_u32())) + } else { + let (_, envelope) = store + .get_record(&coord) + .ok_or_else(|| format!("open: no record at {path}"))?; + let pt = env + .open(store, Selector::Slot(0), &envelope) + .map_err(|e| format!("open: {e:?}"))?; + let cell = CodeCell::from_record(coord, &CubeHeader::new(), &pt) + .ok_or_else(|| "open: decrypted body is not valid bytecode".to_string())?; + // The VM runs code located by coordinate, so to execute a sealed + // record we decrypt it back into a plaintext record, then run. + store.put_record(coord, &CubeHeader::new(), &pt); + let mut vm = Vm::new(store.clone()); + let res = vm.run(cell.label); + Ok(format!( + "open+run {path} (key {}) => {res:?}", + kc.pack_u32() + )) + } + } + other => Err(format!("unknown command: {other}")), + } +} + +/// Parse a byte token: decimal (`42`) or hex (`0x2a`). +fn parse_byte(t: &str) -> Option { + if let Ok(v) = t.parse::() { + return Some(v); + } + u8::from_str_radix(t.trim_start_matches("0x"), 16).ok() +} + +/// Parse a coordinate `C.Z.Y.X` (decimal, allows 0 for Null space). +fn parse_coord(s: &str) -> Option { + let parts: Vec<&str> = s.split('.').collect(); + if parts.len() != 4 { + return None; + } + let nums: Option> = parts.iter().map(|p| p.parse::().ok()).collect(); + let nums = nums?; + Some(cubecoords::Czyx::new(nums[0], nums[1], nums[2], nums[3])) +} + +/// Parse a cubevm op name (case-insensitive) into an [`Op`]. `arg` is the +/// operand byte for ops that take one (const/load/store/jmp/jz/jnz/call/ret/ +/// syscall); it is ignored for argument-less ops. +fn make_op(t: &str, arg: u8) -> Result { + Ok(match t.to_ascii_lowercase().as_str() { + "nop" => Op::Nop, + "halt" => Op::Halt, + "const" => Op::Const(arg), + "load" => Op::Load(arg), + "store" => Op::Store(arg), + "add" => Op::Add, + "sub" => Op::Sub, + "mul" => Op::Mul, + "div" => Op::Div, + "mod" => Op::Mod, + "and" => Op::And, + "or" => Op::Or, + "xor" => Op::Xor, + "shl" => Op::Shl, + "shr" => Op::Shr, + "eq" => Op::Eq, + "ne" => Op::Ne, + "lt" => Op::Lt, + "gt" => Op::Gt, + "le" => Op::Le, + "ge" => Op::Ge, + "jmp" => Op::Jmp(arg), + "jz" => Op::Jz(arg), + "jnz" => Op::Jnz(arg), + "call" => Op::CallLink(arg), + "ret" => Op::Ret, + "syscall" => Op::Syscall(arg), + other => return Err(format!("prog: unknown op {other}")), + }) +} + +/// True for ops that consume the next token as a u8 operand. +fn takes_arg(t: &str) -> bool { + matches!( + t.to_ascii_lowercase().as_str(), + "const" | "load" | "store" | "jmp" | "jz" | "jnz" | "call" | "syscall" + ) +} + +fn parse_transform(s: &str) -> Option { + match s { + "none" => Some(TransformId::None), + "gcm" => Some(TransformId::Aes256Gcm), + "chacha" => Some(TransformId::ChaCha20Poly1305), + "xts" => Some(TransformId::Aes256Xts), + _ => None, + } +} diff --git a/cubesys/src/bin/cube_demo.rs b/cubesys/src/bin/cube_demo.rs new file mode 100644 index 0000000..9599419 --- /dev/null +++ b/cubesys/src/bin/cube_demo.rs @@ -0,0 +1,5 @@ +//! `cube-demo` binary: runs the shared integration demo (see `cubesys::demo`). + +fn main() { + cubesys::demo::run(); +} diff --git a/cubesys/src/lib.rs b/cubesys/src/lib.rs new file mode 100644 index 0000000..4e40160 --- /dev/null +++ b/cubesys/src/lib.rs @@ -0,0 +1,371 @@ +//! CUBELinux-2 system integration — Packages 3-5 bound into one running +//! system over a single [`CubeStore`] (PDF Packages 3-5). +//! +//! The five crates in CUBELinux-2 are deliberately separable. This crate is +//! the composition layer the PDF implies but never names as a "Package 6": +//! it proves the packages interoperate over a *shared* store, exactly as the +//! original design intends ("data addressed by coordinate not path"). +//! +//! The binding it enforces: +//! +//! * **cubefs** lays a POSIX namespace over the cube. Files live at +//! `C/Z/Y/X` paths; each file/record owns a [`Czyx`] coordinate. +//! * **cubecode** stores code cells *as records* in the very same cube — so a +//! file path and a code cell address are the same coordinate. A function +//! written through cubefs at `/c012/z000/y000/x021` IS the cell the VM runs +//! at `Czyx::new(12,0,0,21)`. +//! * **cubecrypt** seals records under a [`CubeEnv`]: the key material lives +//! in Null cubes of the *same* store, so the same coordinate opens to +//! different plaintext under a different environment. +//! +//! Two primitives tie them together: +//! +//! * [`load_code_cell`] — given a cubefs path, read that record and decode it +//! into a [`cubecode::CodeCell`]. This is the path→code bridge: a user +//! drops a `.fn` file, the VM runs it, no coordinate bookkeeping required. +//! * [`path_to_czyx`] — the single bijection (re-exported from `cubefs`) so +//! every layer agrees on where a path lands in the cube. +//! +//! The big design decision (documented in `docs/integration.md`): the cube is +//! the single source of truth. cubefs is a *view* of the cube, cubecode is a +//! *kind* of record in it, and cubecrypt is a *transform* applied to records +//! in it. None of them own storage; the store does. That is the whole point of +//! "coordinate-addressed" — and it is what this crate makes runnable. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +use cubecode::{CodeCell, Kind}; +use cubecoords::{CubeHeader, Czyx}; +use cubefs::path::parse_path; +use cubestore::{CubeBackend, CubeStore, HashBackend}; + +/// The error type for system-level operations that span packages. +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum SysError { + /// The path did not parse as a cube path. + BadPath(String), + /// No record exists at the resolved coordinate (plain or encrypted). + MissingRecord(Czyx), + /// The record's body is not valid cubevm bytecode. + NotCode(Czyx), + /// The cubecrypt envelope failed to open (wrong env / tampered / bad + /// magic). Carries a human reason. + Crypto(String), +} + +impl std::fmt::Display for SysError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SysError::BadPath(s) => write!(f, "bad path: {s}"), + SysError::MissingRecord(c) => { + write!(f, "no record at coord {}", c.pack_u32()) + } + SysError::NotCode(c) => { + write!(f, "record at coord {} is not valid bytecode", c.pack_u32()) + } + SysError::Crypto(s) => write!(f, "crypto error: {s}"), + } + } +} + +/// Resolve a POSIX path to its [`Czyx`] coordinate using the cubefs bijection. +/// +/// This is the canonical entry point every layer should use so path and +/// coordinate never disagree. `c000` and friends are rejected (Null space is +/// not a path), so a returned coordinate is always a user-addressable record +/// or directory prefix. +pub fn path_to_czyx(path: &str) -> Result { + let parsed = parse_path(path).map_err(|e| SysError::BadPath(e.to_string()))?; + parsed + .czyx() + .ok_or_else(|| SysError::BadPath(format!("{path:?} names a directory, not a record"))) +} + +/// Load (and decode) the code cell stored at a cubefs file path. +/// +/// This is the bridge that makes "a file is a function" literally true: it +/// reads the record at the path's coordinate through the *same* store cubefs +/// uses, then decodes the body as cubevm bytecode via [`cubecode`]. Returns the +/// typed [`CodeCell`] so callers get the kind, name, links and decoded ops. +pub fn load_code_cell( + store: &CubeStore, + path: &str, +) -> Result { + let coord = path_to_czyx(path)?; + let (header, body) = store + .get_record(&coord) + .ok_or(SysError::MissingRecord(coord))?; + // Prefer a code cell if the record carries a code doc_type; otherwise try + // to decode whatever is there (the cube is code-as-data, so a raw record + // body may still be valid bytecode). + if let Some(cell) = CodeCell::from_record(coord, &header, &body) { + Ok(cell) + } else { + Err(SysError::NotCode(coord)) + } +} + +/// Build a [`CodeCell`] from parts and write it back as a record at `path`, +/// so code written by the VM (or tooling) is visible to cubefs at that path. +pub fn store_code_cell( + store: &mut CubeStore, + path: &str, + kind: Kind, + name: &str, + links: &[Czyx], + code: &[cubecode::Op], +) -> Result { + let coord = path_to_czyx(path)?; + let mut h = CubeHeader::new(); + h.title = Some(name.to_string()); + h.doc_type = Some(kind.as_str().to_string()); + h.linked_records = links.to_vec(); + if h.doc_type.as_deref() == Some("fn") { + h.size_bytes = Some(cubecode::encode(code).len() as u64); + } + h.refresh_flags(); + store.put_record(coord, &h, &cubecode::encode(code)); + Ok(coord) +} + +/// Convenience: an in-memory system store, the default for demos/tests. +pub fn memory_store() -> CubeStore { + CubeStore::new(HashBackend::new()) +} + +#[cfg(test)] +mod tests { + use super::*; + use cubecode::Op; + use cubecoords::Czyx; + use cubecrypt::{CubeEnv, KeySlot, Selector, TransformId, HEADER_FLAG_ENCRYPTED}; + + #[test] + fn path_is_the_code_cell_coordinate() { + // The headline property: a cubefs path and a code cell resolve to the + // SAME coordinate, so the VM runs what cubefs lists as a file. + // Note: axis value 0 is Null control space and is NOT a valid path + // component, so every axis here is non-zero. + let coord = path_to_czyx("/c012/z001/y001/x021").unwrap(); + assert_eq!(coord, Czyx::new(12, 1, 1, 21)); + + let mut store = memory_store(); + let path = "/c012/z001/y001/x021"; + store_code_cell( + &mut store, + path, + Kind::Fn, + "add", + &[], + &[Op::Const(2), Op::Const(3), Op::Add, Op::Halt], + ) + .unwrap(); + + // cubefs sees it as a file at that path... + let fs = cubefs::CubeFs::new(store.clone()); + assert_eq!(fs.kind_of(path).unwrap(), cubefs::Kind::File); + + // ...and cubecode decodes the very same record as a runnable cell. + let cell = load_code_cell(&store, path).unwrap(); + assert_eq!(cell.kind(), Kind::Fn); + assert_eq!(cell.name(), Some("add")); + assert_eq!( + cell.code, + vec![Op::Const(2), Op::Const(3), Op::Add, Op::Halt] + ); + + // run it through the VM on the shared store + let mut vm = cubecode::Vm::new(store.clone()); + assert_eq!(vm.run(coord), cubecode::RunResult::Halted { top: Some(5) }); + } + + #[test] + fn encrypt_then_run_is_the_same_record() { + // Encrypt a record via cubecrypt, confirm the same coordinate now + // carries an encrypted body, then decrypt and run it. + let mut store = memory_store(); + let env = CubeEnv::new( + vec![KeySlot { + key_cell: Czyx::new(0, 1, 0, 1), // Null cube 1 + transform: TransformId::Aes256Gcm, + salt: vec![], + }], + vec![], + ); + store.put_record( + Czyx::new(0, 1, 0, 1), + &CubeHeader::new(), + b"environment-root-key-32bytes!!", + ); + + let coord = path_to_czyx("/c005/z001/y001/x007").unwrap(); + // plaintext bytecode first + let code = vec![Op::Const(7), Op::Halt]; + store.put_record(coord, &plaintext_header(), &cubecode::encode(&code)); + assert!(load_code_cell(&store, "/c005/z001/y001/x007").is_ok()); + + // now seal the SAME coordinate under the env + env.put_encrypted( + &mut store, + coord, + Selector::Slot(0), + &cubecode::encode(&code), + CubeHeader::new(), + ) + .unwrap(); + let (h, _body) = store.get_record(&coord).unwrap(); + assert!(h.flags.has(HEADER_FLAG_ENCRYPTED)); + // the raw record body is no longer plaintext bytecode + assert!(load_code_cell(&store, "/c005/z001/y001/x007").is_err()); + + // decrypt + decode + run on the shared store + let envelope = store.get_record(&coord).unwrap().1; + let pt = env.open(&store, Selector::Slot(0), &envelope).unwrap(); + let cell = CodeCell::from_record(coord, &CubeHeader::new(), &pt).unwrap(); + assert_eq!(cell.code, code); + // The VM runs code located by coordinate, so to *execute* a sealed + // record we decrypt it back into a plaintext record at the same coord, + // then run. (Reading the encrypted envelope directly would fail decode.) + store.put_record(coord, &CubeHeader::new(), &pt); + let mut vm = cubecode::Vm::new(store.clone()); + assert_eq!(vm.run(coord), cubecode::RunResult::Halted { top: Some(7) }); + } + + fn plaintext_header() -> CubeHeader { + let mut h = CubeHeader::new(); + h.doc_type = Some("fn".into()); + h.refresh_flags(); + h + } +} + +/// The integration demo: build ONE in-memory [`CubeStore`] and run cubefs, +/// cubecode and cubecrypt over it, printing evidence at each step. +/// +/// See `docs/integration.md`. Shared by the `cube` and `cube-demo` binaries. +pub mod demo { + use cubecode::{Kind, Op, Vm}; + use cubecoords::Czyx; + use cubecrypt::{CubeEnv, KeySlot, Selector, TransformId}; + use cubestore::{CubeStore, HashBackend}; + + /// Run the integration tour. + pub fn run() { + println!("=== CUBELinux-2 integration demo ===\n"); + + let mut store = CubeStore::new(HashBackend::new()); + + // ---- 1. write code cells as cubefs paths, run through the VM ---------- + println!("[1] cubefs + cubecode: a path IS a code cell"); + let double = "/c002/z001/y001/x001"; + let entry = "/c001/z001/y001/x001"; + + let double_code = vec![Op::Store(0), Op::Load(0), Op::Const(2), Op::Mul, Op::Ret]; + let entry_code = vec![Op::Const(21), Op::CallLink(0), Op::Halt]; + + let double_coord = + super::store_code_cell(&mut store, double, Kind::Fn, "double", &[], &double_code) + .expect("store double"); + let entry_coord = super::store_code_cell( + &mut store, + entry, + Kind::Fn, + "entry", + &[double_coord], + &entry_code, + ) + .expect("store entry"); + println!(" wrote {double} -> coord {}", double_coord.pack_u32()); + println!(" wrote {entry} -> coord {}", entry_coord.pack_u32()); + println!( + " entry links to double at coord {}", + double_coord.pack_u32() + ); + + // cubefs sees both as files in the same cube + let fs = cubefs::CubeFs::new(store.clone()); + let root = fs.readdir("/").expect("readdir /"); + println!( + " cubefs '/' lists: {}", + root.iter() + .map(|(n, _)| n.clone()) + .collect::>() + .join(" ") + ); + + // load the entry cell the same way the CLI would, and run it + let cell = super::load_code_cell(&store, entry).expect("load entry"); + let mut vm = Vm::new(store.clone()); + let res = vm.run(cell.label); + println!(" VM ran {entry}: {res:?} (expected Halted 42)"); + assert_eq!(res, cubecode::RunResult::Halted { top: Some(42) }); + + // ---- 2. cubecrypt: seal a record, then open + run it ----------------- + println!("\n[2] cubecrypt: same coordinate, sealed + reopened"); + // Key material lives in Null space (axis value 0) — not a cubefs path. + let key_cell = Czyx::new(0, 1, 0, 1); + store.put_record( + key_cell, + &cubecoords::CubeHeader::new(), + b"demo-key-material-32-bytes-long!!", + ); + let env = CubeEnv::new( + vec![KeySlot { + key_cell, + transform: TransformId::Aes256Gcm, + salt: vec![], + }], + vec![], + ); + + let sealed = "/c003/z001/y001/x005"; + let sealed_coord = super::path_to_czyx(sealed).expect("parse sealed path"); + // store plaintext bytecode, then seal the same coordinate + store.put_record( + sealed_coord, + &cubecoords::CubeHeader::new(), + &cubecode::encode(&[Op::Const(9), Op::Halt]), + ); + env.put_encrypted( + &mut store, + sealed_coord, + Selector::Slot(0), + &cubecode::encode(&[Op::Const(9), Op::Halt]), + cubecoords::CubeHeader::new(), + ) + .expect("seal"); + let (h, _) = store.get_record(&sealed_coord).expect("read sealed"); + println!( + " sealed {sealed}: header encrypted flag set = {}", + h.flags.has(cubecrypt::HEADER_FLAG_ENCRYPTED) + ); + + // the raw record is no longer readable as plaintext bytecode + let raw = store.get_record(&sealed_coord).unwrap().1; + let still_plain = + cubecode::CodeCell::from_record(sealed_coord, &cubecoords::CubeHeader::new(), &raw); + println!( + " raw record decodes as bytecode? {}", + still_plain.is_some() + ); + + // open + decode + run on the shared store. The VM runs code located by + // coordinate, so to *execute* the sealed record we decrypt it back into a + // plaintext record at the same coord, then run. + let envelope = store.get_record(&sealed_coord).unwrap().1; + let pt = env + .open(&store, Selector::Slot(0), &envelope) + .expect("open"); + let opened = + cubecode::CodeCell::from_record(sealed_coord, &cubecoords::CubeHeader::new(), &pt) + .expect("decode decrypted bytecode"); + store.put_record(sealed_coord, &cubecoords::CubeHeader::new(), &pt); + let mut vm2 = Vm::new(store.clone()); + let res2 = vm2.run(opened.label); + println!(" opened + ran {sealed}: {res2:?} (expected Halted 9)"); + assert_eq!(res2, cubecode::RunResult::Halted { top: Some(9) }); + + println!("\n=== demo OK: one cube, three packages, same coordinates ==="); + } +}