Files
omnivox-rs/examples/dump-viewer.rs
T
michael fea8ea3ab1 0.15.0: simple builder API, v2.1 framing, stream lane, benches
- omnivox::server::<T>()/client::<T>() builders; plain closures, enum in,
  match in the callback — the original C++ UX with the ceremony removed
- serde + bincode replace the hand-rolled Cereal codec (vectors.rs still
  governs the crypto leaves via the C++ golden oracles)
- v2.1 frames: u32 len | type | ct | tag; nonce derived from the sequence
  counter (no wire nonce), type byte authenticated as AAD; any frame-level
  failure is fatal, authenticated-but-undecodable payloads are skipped
- stream lane: send_stream/on_stream, 1 MiB frames, shared sequence with
  messages (exact sender interleaving preserved); see dump-agent/viewer
- per-connection bounded queues + writer tasks; reply/reply_stream are
  non-blocking try_send lanes for callbacks
- benches: seal/open criterion group + transport harness (spray msg/s,
  stream GiB/s, raw loopback baseline)
- profile.dev.package scrypt/pbkdf2 opt-level 3: golden-vector scrypt at
  N=2^17 was unusable in debug builds
- PROTOCOL-V2.md and README rewritten for v2.1
2026-09-30 08:07:38 -04:00

85 lines
2.9 KiB
Rust

//! The dump **viewer** side: receives an encrypted memory stream while
//! control messages arrive on the same session, in order, interleaved.
//!
//! Run this first, then `cargo run --example dump-agent`. Pages are
//! appended to `/tmp/omnivox-dump.bin`; the `FNV-1a` hash printed at the
//! end proves byte-exact arrival (run the agent twice — same hash).
use omnivox::Config;
use serde::{Deserialize, Serialize};
use std::io::Write;
use std::sync::{Arc, Mutex};
#[derive(Serialize, Deserialize, Debug)]
enum AgentMsg {
Progress { page: u32, pages: u32 },
Done { bytes: u64 },
}
/// Running FNV-1a over everything the stream lane delivers.
#[derive(Default)]
struct Digest(u64);
impl Digest {
fn add(&mut self, bytes: &[u8]) {
for &b in bytes {
self.0 ^= b as u64;
self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3);
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_target(false)
.without_time()
.init();
// Shared across both callbacks (and the Done handler) — a plain
// Arc<Mutex<..>> like any app state.
let digest = Arc::new(Mutex::new(Digest::default()));
let file = Arc::new(Mutex::new(std::fs::File::create("/tmp/omnivox-dump.bin")?));
let _server = omnivox::server::<AgentMsg>()
.password("SecurePassword123", b"static-demo-salt")
// Larger stream frames than the default, matching the agent.
.config(Config {
max_stream_frame: 1024 * 1024,
..Config::default()
})
.on_data({
let file = Arc::clone(&file);
let digest = Arc::clone(&digest);
move |msg, _ctx| match msg {
AgentMsg::Progress { page, pages } => {
tracing::info!("page {page}/{pages} inbound");
file.lock().unwrap().write_all(b"\n").ok(); // page boundary marker
}
AgentMsg::Done { bytes } => {
tracing::info!(
"dump complete: {bytes} bytes, fnv1a={:016x}, saved to /tmp/omnivox-dump.bin",
digest.lock().unwrap().0
);
std::process::exit(0);
}
}
})
// The bulk lane: raw decrypted bytes, zero serde, zero copies beyond
// the queue hop. This is where multi-GB dumps land.
.on_stream({
let file = Arc::clone(&file);
let digest = Arc::clone(&digest);
move |chunk: &[u8], _ctx| {
digest.lock().unwrap().add(chunk);
file.lock().unwrap().write_all(chunk).ok();
}
})
.serve(8080)
.await?;
tracing::info!("viewer listening on :8080, waiting for agent...");
std::future::pending::<()>().await;
Ok(())
}