//! Shared CUBELinux-2 system command interpreter. //! //! This module holds the *single* implementation of the cube command language //! (`prog`, `write`, `run`, `ls`, `stat`, `seal`, `open`). It is used by every //! front-end — the local `cube` REPL, the `cubec` socket client, and the //! `cube-server` daemon — so the behaviour can never drift between them. //! //! A [`Session`] wraps one [`CubeStore`] backend (currently the in-memory //! [`HashBackend`]) and executes one command line at a time against it. The //! daemon holds a single long-lived `Session`; the REPL holds a transient one. use cubecode::{CodeCell, Kind, Op, Vm}; use cubecoords::CubeHeader; use cubecrypt::{CubeEnv, KeySlot, Selector, TransformId}; use cubestore::{CubeStore, HashBackend}; /// One cube command session: a store plus the command interpreter. pub struct Session { store: CubeStore, } impl Default for Session { fn default() -> Self { Self::new() } } impl Session { /// A fresh, empty session over an in-memory store. pub fn new() -> Self { Session { store: CubeStore::new(HashBackend::new()), } } /// Access the underlying store (used by persistence to snapshot/restore). pub fn store(&self) -> &CubeStore { &self.store } /// Mutable access to the underlying store (used by persistence to load). pub fn store_mut(&mut self) -> &mut CubeStore { &mut self.store } /// Execute one command line. `Ok(out)` is a (possibly multi-line) result to /// print; `Err(e)` is a human-readable error. pub fn exec(&mut self, line: &str) -> Result { let mut it = line.split_whitespace(); let cmd = it.next().ok_or_else(|| "empty line".to_string())?; match cmd { "prog" => { 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 = crate::store_code_cell(&mut self.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 = crate::store_code_cell(&mut self.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 = crate::load_code_cell(&self.store, path).map_err(|e| e.to_string())?; let mut vm = Vm::new(self.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(self.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(self.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 = crate::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 self.store.get_record(&kc).is_none() { self.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) = self .store .get_record(&coord) .ok_or_else(|| format!("seal: no record at {path}"))?; env.put_encrypted(&mut self.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) = self .store .get_record(&coord) .ok_or_else(|| format!("open: no record at {path}"))?; let pt = env .open(&self.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. self.store.put_record(coord, &CubeHeader::new(), &pt); let mut vm = Vm::new(self.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). pub 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" ) } /// Parse a transform token into a [`TransformId`]. pub 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, } }