use std::path::{Path, PathBuf};
use std::process::ExitCode;
use verit::{
codegen, dump_json, dump_json_with, Budget, FileBuilder, FileView, FileWriter, Message,
Resolver,
};
const USAGE: &str = "\
verit — Exavian Veritate CLI
USAGE:
verit <COMMAND> [ARGS]
FILES (.verit)
ls <file> Overview: generation, records, schemas, live/dead space.
pack <msg>... -o <f> Build a .verit file from self-describing messages.
unpack <file> [-o <d>] Write each record to <d>/, plus the schema bundle.
compact <file> Reclaim dead space. ERASES removed records — see below.
FILES OR MESSAGES (dispatched on magic)
dump <path> JSON. One line per record for a file; one document for a message.
--record <N> or --id <N> selects a single record of a file.
id <path> The 128-bit schema id — one per record for a file.
verify <path> Structural check plus a decode of every record.
MESSAGES (.bin)
gen <msg> Emit a typed Rust reader/writer for the message's inline schema.
SCHEMAS (.vsc)
build <schema.vsc> Print the schema id, or with --lang rust|python|ts|go|cpp
emit typed bindings to stdout.
NOTES
A .verit file is self-contained: it carries the schemas for its own records,
so dump/verify need nothing else. A bare message needs an inline schema
(SchemaMode::Inline) for the schema-aware commands.
`compact` is the only command that erases: removing a record unlinks it, but
its bytes stay in the file until a compaction rewrites it.
";
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
match run(&args) {
Ok(out) => {
print!("{out}");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("verit: {e}");
ExitCode::FAILURE
}
}
}
enum Artifact {
File(Vec<u8>),
Message(Vec<u8>),
}
fn classify(path: &str) -> Result<Artifact, String> {
let bytes = std::fs::read(path).map_err(|e| format!("reading {path}: {e}"))?;
match bytes.get(0..4) {
Some(b"VRTF") => Ok(Artifact::File(bytes)),
Some(b"VRT2") => Ok(Artifact::Message(bytes)),
Some(b"VRTC") => Err(format!(
"{path} is a deprecated .vertc container, not a .verit file \
(superseded in 0.2.0; see ADR-0002)"
)),
Some(b"VRSB") => Err(format!("{path} is a schema bundle, not a file or message")),
_ => Err(format!(
"{path} is not a Veritate artifact (expected magic VRTF or VRT2)"
)),
}
}
fn run(args: &[String]) -> Result<String, String> {
let cmd = args.first().map(String::as_str).unwrap_or("");
if matches!(cmd, "" | "-h" | "--help" | "help") {
return Ok(USAGE.to_string());
}
if matches!(cmd, "-V" | "--version" | "version") {
return Ok(format!("verit {}\n", verit::VERSION));
}
match cmd {
"build" => return build_cmd(&args[1..]),
"pack" => return pack_cmd(&args[1..]),
"unpack" => return unpack_cmd(&args[1..]),
"compact" => return compact_cmd(&args[1..]),
_ => {}
}
let path = args
.get(1)
.ok_or_else(|| format!("`{cmd}` needs a path\n\n{USAGE}"))?;
match cmd {
"ls" => match classify(path)? {
Artifact::File(bytes) => ls_file(path, &bytes),
Artifact::Message(_) => {
Err("`ls` works on a .verit file; use `dump` for a message".into())
}
},
"dump" => match classify(path)? {
Artifact::File(bytes) => dump_file(&bytes, &args[2..]),
Artifact::Message(bytes) => Ok(format!(
"{}\n",
dump_json(&bytes).map_err(|e| e.to_string())?
)),
},
"id" => match classify(path)? {
Artifact::File(bytes) => {
let f = FileView::open(&bytes).map_err(|e| e.to_string())?;
let mut out = String::new();
for r in f.records() {
out.push_str(&format!("{}\t{:032x}\n", r.id, r.schema_id));
}
Ok(out)
}
Artifact::Message(bytes) => {
let msg = Message::parse(&bytes).map_err(|e| e.to_string())?;
Ok(format!("{:032x}\n", msg.schema_id()))
}
},
"verify" => match classify(path)? {
Artifact::File(bytes) => verify_file(path, &bytes),
Artifact::Message(bytes) => verify_message(&bytes),
},
"gen" => match classify(path)? {
Artifact::Message(bytes) => {
let msg = Message::parse(&bytes).map_err(|e| e.to_string())?;
let schema = msg
.writer_schema()
.map_err(|e| e.to_string())?
.ok_or("message has no inline schema to generate from")?;
codegen::generate_rust(&schema).map_err(|e| e.to_string())
}
Artifact::File(_) => Err(
"`gen` works on a message; a file may hold several schemas — \
unpack it, or use `ls` to see them"
.into(),
),
},
other => Err(format!("unknown command `{other}`\n\n{USAGE}")),
}
}
fn ls_file(path: &str, bytes: &[u8]) -> Result<String, String> {
let f = FileView::open(bytes).map_err(|e| e.to_string())?;
let footer = f.footer();
let mut out = String::new();
out.push_str(&format!("file: {path}\n"));
out.push_str(&format!("generation: {}\n", f.generation()));
out.push_str(&format!("records: {}\n", f.len()));
out.push_str(&format!("next id: {}\n", f.next_record_id()));
let live: u64 = f.records().map(|r| r.length).sum();
let index = f.len() as u64 * 40;
let overhead = 32 + footer.schema_len as u64 + index + 64;
let dead = f.file_len().saturating_sub(live + overhead);
out.push_str(&format!(
"size: {} bytes (live {live}, format {overhead}, dead {dead})\n",
f.file_len()
));
if bytes.len() as u64 > f.file_len() {
out.push_str(&format!(
" + {} uncommitted bytes past the committed extent \
(a torn commit; harmless, cleared on next write)\n",
bytes.len() as u64 - f.file_len()
));
}
if dead * 10 > f.file_len() {
out.push_str(&format!(
" {dead} dead bytes ({}%) — `verit compact {path}` reclaims them\n",
dead * 100 / f.file_len().max(1)
));
}
out.push_str(&format!("\nschemas: {}\n", f.schemas().len()));
let mut ids: Vec<u128> = f.schemas().ids().collect();
ids.sort_unstable();
for id in ids {
let schema = f.schemas().get(id).unwrap();
let uses = f.records().filter(|r| r.schema_id == id).count();
out.push_str(&format!(
" {:032x} {:<16} {uses} record(s)\n",
id,
schema.type_name(schema.root_index())
));
}
if f.is_empty() {
return Ok(out);
}
out.push_str("\n id bytes schema json\n");
for i in 0..f.len() {
let r = f.record(i).unwrap();
let json = f.dump_json(i).unwrap_or_else(|e| format!("<{e}>"));
out.push_str(&format!(
"{:>5}{:>7} {:032x} {}\n",
r.id,
r.length,
r.schema_id,
truncate(&json, 60)
));
}
Ok(out)
}
fn truncate(s: &str, n: usize) -> String {
if s.chars().count() <= n {
return s.to_string();
}
let head: String = s.chars().take(n - 1).collect();
format!("{head}…")
}
fn dump_file(bytes: &[u8], rest: &[String]) -> Result<String, String> {
let f = FileView::open(bytes).map_err(|e| e.to_string())?;
let mut selected: Option<usize> = None;
let mut it = rest.iter();
while let Some(a) = it.next() {
let value = |it: &mut std::slice::Iter<'_, String>| -> Result<u64, String> {
it.next()
.ok_or_else(|| format!("{a} needs a value"))?
.parse::<u64>()
.map_err(|e| format!("{a}: {e}"))
};
match a.as_str() {
"--record" => {
let n = value(&mut it)? as usize;
if n >= f.len() {
return Err(format!("--record {n}: file has {} record(s)", f.len()));
}
selected = Some(n);
}
"--id" => {
let id = value(&mut it)?;
selected = Some(
f.find_by_id(id)
.ok_or_else(|| format!("--id {id}: no live record with that id"))?,
);
}
other => return Err(format!("unknown flag `{other}` for dump\n\n{USAGE}")),
}
}
let mut out = String::new();
match selected {
Some(i) => out.push_str(&format!("{}\n", f.dump_json(i).map_err(|e| e.to_string())?)),
None => {
for i in 0..f.len() {
out.push_str(&format!("{}\n", f.dump_json(i).map_err(|e| e.to_string())?));
}
}
}
Ok(out)
}
fn verify_file(path: &str, bytes: &[u8]) -> Result<String, String> {
let f = FileView::open(bytes).map_err(|e| e.to_string())?;
let mut out = String::new();
if bytes.len() as u64 > f.file_len() {
out.push_str(&format!(
"RECOVERED: the last commit was incomplete ({} bytes past the committed extent). \
Reading generation {}; the interrupted commit was rolled back.\n",
bytes.len() as u64 - f.file_len(),
f.generation()
));
}
let mut budgeted = 0usize;
for i in 0..f.len() {
let schema = f.schema(i).map_err(|e| format!("record {i}: {e}"))?;
let record = f.get(i).map_err(|e| format!("record {i}: {e}"))?;
let msg = Message::parse(record).map_err(|e| format!("record {i}: {e}"))?;
let resolver = Resolver::identity(schema).map_err(|e| format!("record {i}: {e}"))?;
let budget = Budget::new(msg.suggested_budget());
msg.verify(&resolver, &budget)
.map_err(|e| format!("record {i} (id {}): {e}", f.record(i).unwrap().id))?;
dump_json_with(schema, record).map_err(|e| format!("record {i}: {e}"))?;
budgeted += 1;
}
out.push_str(&format!(
"ok: {path}\n generation {}, {} record(s), {} schema(s), {} bytes\n \
{budgeted} record(s) decoded within budget, using only this file\n",
f.generation(),
f.len(),
f.schemas().len(),
f.file_len()
));
Ok(out)
}
fn verify_message(bytes: &[u8]) -> Result<String, String> {
let msg = Message::parse(bytes).map_err(|e| e.to_string())?;
let schema = msg
.writer_schema()
.map_err(|e| e.to_string())?
.ok_or("message has no inline schema to verify against")?;
let resolver = Resolver::identity(&schema).map_err(|e| e.to_string())?;
let budget = Budget::new(msg.suggested_budget());
msg.verify(&resolver, &budget).map_err(|e| e.to_string())?;
Ok(format!(
"ok: {} bytes, traversal within budget ({} of {} bytes left)\n",
bytes.len(),
budget.remaining(),
msg.suggested_budget()
))
}
fn pack_cmd(rest: &[String]) -> Result<String, String> {
let mut inputs: Vec<PathBuf> = Vec::new();
let mut out_path: Option<PathBuf> = None;
let mut it = rest.iter();
while let Some(a) = it.next() {
match a.as_str() {
"-o" | "--output" => {
out_path = Some(PathBuf::from(it.next().ok_or("-o needs a path")?));
}
s if s.starts_with('-') => return Err(format!("unknown flag `{s}`\n\n{USAGE}")),
s => inputs.push(PathBuf::from(s)),
}
}
let out_path = out_path.ok_or("pack needs an output: -o <out.verit>")?;
if inputs.is_empty() {
return Err("pack needs at least one message file".into());
}
let mut b = FileBuilder::new();
for input in &inputs {
let bytes =
std::fs::read(input).map_err(|e| format!("reading {}: {e}", input.display()))?;
b.append_self_describing(&bytes).map_err(|e| {
format!(
"{}: {e} (pack needs messages encoded with an inline schema)",
input.display()
)
})?;
}
let image = b.finish().map_err(|e| e.to_string())?;
std::fs::write(&out_path, &image)
.map_err(|e| format!("writing {}: {e}", out_path.display()))?;
Ok(format!(
"packed {} message(s) into {} ({} bytes)\n",
inputs.len(),
out_path.display(),
image.len()
))
}
fn unpack_cmd(rest: &[String]) -> Result<String, String> {
let mut path: Option<&str> = None;
let mut dir: Option<PathBuf> = None;
let mut it = rest.iter();
while let Some(a) = it.next() {
match a.as_str() {
"-o" | "--output" => dir = Some(PathBuf::from(it.next().ok_or("-o needs a path")?)),
s if s.starts_with('-') => return Err(format!("unknown flag `{s}`\n\n{USAGE}")),
s if path.is_none() => path = Some(s),
_ => return Err("unpack takes exactly one file".into()),
}
}
let path = path.ok_or("unpack needs a .verit file")?;
let bytes = match classify(path)? {
Artifact::File(b) => b,
Artifact::Message(_) => return Err("unpack works on a .verit file, not a message".into()),
};
let f = FileView::open(&bytes).map_err(|e| e.to_string())?;
let dir = dir.unwrap_or_else(|| PathBuf::from(Path::new(path).file_stem().unwrap_or_default()));
std::fs::create_dir_all(&dir).map_err(|e| format!("creating {}: {e}", dir.display()))?;
for i in 0..f.len() {
let r = f.record(i).unwrap();
let name = dir.join(format!("{:06}.bin", r.id));
std::fs::write(&name, f.get(i).unwrap())
.map_err(|e| format!("writing {}: {e}", name.display()))?;
}
let bundle_path = dir.join("schemas.vrsb");
std::fs::write(&bundle_path, f.schemas().to_bundle())
.map_err(|e| format!("writing {}: {e}", bundle_path.display()))?;
Ok(format!(
"unpacked {} record(s) to {}/ (named by record id), plus {} schema(s) in schemas.vrsb\n\
note: records are stored hash-only, so they are not self-describing on their own — \
the bundle is what makes the set readable\n",
f.len(),
dir.display(),
f.schemas().len()
))
}
fn compact_cmd(rest: &[String]) -> Result<String, String> {
let path = rest
.first()
.ok_or_else(|| format!("compact needs a .verit file\n\n{USAGE}"))?;
match classify(path)? {
Artifact::File(_) => {}
Artifact::Message(_) => return Err("compact works on a .verit file, not a message".into()),
}
let before = std::fs::metadata(path)
.map_err(|e| format!("{path}: {e}"))?
.len();
let mut w = FileWriter::open(path).map_err(|e| e.to_string())?;
let records = w.len();
w.compact().map_err(|e| e.to_string())?;
let after = std::fs::metadata(path)
.map_err(|e| format!("{path}: {e}"))?
.len();
Ok(format!(
"compacted {path}: {before} → {after} bytes ({} reclaimed), {records} record(s) kept\n\
record ids and the id counter are preserved; positions are not\n\
any previously removed record is now ERASED from this file — \
backups and snapshots taken before now are not covered\n",
before.saturating_sub(after)
))
}
fn build_cmd(rest: &[String]) -> Result<String, String> {
let mut path: Option<&str> = None;
let mut lang: Option<String> = None;
let mut it = rest.iter();
while let Some(a) = it.next() {
match a.as_str() {
"--lang" => {
lang = Some(
it.next()
.ok_or("--lang needs a value (rust|python)")?
.clone(),
);
}
s if s.starts_with("--lang=") => lang = Some(s["--lang=".len()..].to_string()),
s if s.starts_with('-') => return Err(format!("unknown flag `{s}`\n\n{USAGE}")),
s if path.is_none() => path = Some(s),
_ => return Err("build takes exactly one .vsc file".into()),
}
}
let path = path.ok_or_else(|| format!("build needs a .vsc file\n\n{USAGE}"))?;
let src = std::fs::read_to_string(path).map_err(|e| format!("reading {path}: {e}"))?;
let schema = verit::idl::parse(&src).map_err(|e| e.to_string())?;
match lang.as_deref() {
None => Ok(format!(
"schema id: {:032x}\nroot: {}\ntypes: {}\n",
schema.id(),
schema.type_name(schema.root_index()),
schema.type_count()
)),
Some("rust") => verit::codegen::generate_rust(&schema).map_err(|e| e.to_string()),
Some("python") => verit::codegen::generate_python(&schema).map_err(|e| e.to_string()),
Some("ts") | Some("typescript") => {
verit::codegen::generate_ts(&schema).map_err(|e| e.to_string())
}
Some("go") => verit::codegen::generate_go(&schema).map_err(|e| e.to_string()),
Some("cpp") | Some("c++") => {
verit::codegen::generate_cpp(&schema).map_err(|e| e.to_string())
}
Some(other) => Err(format!(
"unknown --lang `{other}` (expected rust|python|ts|go|cpp)"
)),
}
}