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).
This commit is contained in:
@@ -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 <file> # read commands from <file>, one per line
|
||||
//!
|
||||
//! Commands (operate on the shared cube):
|
||||
//! write <path> <bytes...> # store cubevm bytecode (hex/dec) at a path
|
||||
//! run <path> # load the code cell at <path> and run the VM
|
||||
//! ls <dir> # list a cubefs directory
|
||||
//! stat <path> # getattr via cubefs
|
||||
//! seal <path> <K.C.Z.Y.X> <tf> # encrypt the record at <path> (tf: none|gcm|chacha|xts)
|
||||
//! open <path> <K.C.Z.Y.X> <tf> # 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<String> = 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 <file>");
|
||||
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 <file> run commands from a file\n\
|
||||
\nCommands:\n \
|
||||
prog <path> <ops...> write CUBEVM bytecode from op names\n \
|
||||
write <path> <bytes...> store cubevm bytecode at a path\n \
|
||||
run <path> run the code cell at <path>\n \
|
||||
ls <dir> list a cubefs directory\n \
|
||||
stat <path> getattr via cubefs\n \
|
||||
seal <path> <K.Z.Y.X> <tf> encrypt a record (tf: none|gcm|chacha|xts)\n \
|
||||
open <path> <K.Z.Y.X> <tf> decrypt + decode + run a sealed record\n"
|
||||
);
|
||||
}
|
||||
|
||||
/// Execute one command line against the shared store.
|
||||
fn run_line(store: &mut CubeStore<HashBackend>, line: &str) -> Result<String, String> {
|
||||
let mut it = line.split_whitespace();
|
||||
let cmd = it.next().ok_or_else(|| "empty line".to_string())?;
|
||||
match cmd {
|
||||
"prog" => {
|
||||
// prog <path> <ops...> — 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 <path>".to_string())?;
|
||||
let mut ops: Vec<Op> = Vec::new();
|
||||
while let Some(tok) = it.next() {
|
||||
let arg = if takes_arg(tok) {
|
||||
it.next()
|
||||
.and_then(|a| a.parse::<u8>().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 <path>".to_string())?;
|
||||
let bytes: Vec<u8> = it
|
||||
.map(parse_byte)
|
||||
.collect::<Option<_>>()
|
||||
.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 <path>".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 <dir>".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<String> = entries.into_iter().map(|(n, _)| n).collect();
|
||||
Ok(format!("ls {dir} -> {}", names.join(" ")))
|
||||
}
|
||||
}
|
||||
"stat" => {
|
||||
let path = it.next().ok_or_else(|| "stat needs <path>".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 <path>"))?;
|
||||
let keyc = it
|
||||
.next()
|
||||
.ok_or_else(|| format!("{cmd} needs <K.Z.Y.X> key cell"))?;
|
||||
let tf = it
|
||||
.next()
|
||||
.ok_or_else(|| format!("{cmd} needs <transform>"))?;
|
||||
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<u8> {
|
||||
if let Ok(v) = t.parse::<u8>() {
|
||||
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<cubecoords::Czyx> {
|
||||
let parts: Vec<&str> = s.split('.').collect();
|
||||
if parts.len() != 4 {
|
||||
return None;
|
||||
}
|
||||
let nums: Option<Vec<u8>> = parts.iter().map(|p| p.parse::<u8>().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<Op, String> {
|
||||
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<TransformId> {
|
||||
match s {
|
||||
"none" => Some(TransformId::None),
|
||||
"gcm" => Some(TransformId::Aes256Gcm),
|
||||
"chacha" => Some(TransformId::ChaCha20Poly1305),
|
||||
"xts" => Some(TransformId::Aes256Xts),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user