Initial commit: aether-tools-rs workspace

This commit is contained in:
gyt
2026-07-09 13:28:28 -04:00
commit 695725857d
17 changed files with 1912 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "aether-net-mini"
version.workspace = true
edition.workspace = true
[[bin]]
name = "aether-net-mini"
path = "src/main.rs"
[dependencies]
aether-atom = { path = "/home/cosmos/aether-atom" }
serde = { workspace = true }
serde_json = { workspace = true }
sha3 = { workspace = true }
tokio = { workspace = true }
clap = { workspace = true }
[dev-dependencies]
tempfile = "3"
+245
View File
@@ -0,0 +1,245 @@
//! aether-net-mini — DHT abstraction over aether-atom.
//!
//! The original aether-net was 1173 LOC of single-threaded TCP JSON-RPC
//! with a custom in-memory hashmap. This rewrite uses the atom for
//! storage and tokio for the network loop. Every byte of public API
//! (`put`, `get`) is a one-liner over the atom.
//!
//! Reduced surface: 2 methods instead of 15. Persistence is implicit
//! (atom ledger survives restart). Scopes work the same way (atom
//! supports `global` and arbitrary per-repo overrides).
use aether_atom::{fingerprint, Lattice};
use clap::{Parser, Subcommand};
use serde_json::{json, Value};
use std::io::{BufRead, BufReader, Write};
use std::net::TcpStream;
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader as ABufReader};
use tokio::net::TcpListener as ATcpListener;
use tokio::sync::Mutex;
#[derive(Parser, Debug)]
#[command(name = "aether-net-mini", about = "DHT over aether-atom")]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand, Debug)]
enum Cmd {
/// Long-running daemon. Stores all content in aether-atom.
Daemon {
#[arg(long, default_value = "127.0.0.1:0")]
listen: String,
#[arg(long)]
data_dir: Option<String>,
},
/// One-shot client: connect, send a single JSON-RPC request, print
/// response.
Client {
#[arg(long)]
server: String,
#[arg(long)]
method: String,
#[arg(long, default_value = "{}", value_parser = parse_json_arg)]
params: Value,
},
}
fn parse_json_arg(s: &str) -> Result<Value, String> {
serde_json::from_str(s).map_err(|e| e.to_string())
}
fn err_response(id: Value, code: i64, msg: &str) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": msg } })
}
fn ok_response(id: Value, result: Value) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "result": result })
}
/// The two real verbs over aether-atom's lattice:
/// - `put(data)` → hash it, store
/// - `get(hash)` → retrieve
///
/// Subscribe / replicate are future features; not in this surface.
async fn dispatch(
method: &str,
params: Value,
lattice: &mut Lattice,
) -> Result<Value, String> {
match method {
"put" => {
let data = params
.get("data")
.and_then(|v| v.as_str())
.ok_or_else(|| "missing 'data'".to_string())?;
let bytes = data.as_bytes();
let sig = fingerprint(bytes);
lattice.record(sig, "global", bytes, Some(true));
Ok(json!({ "hash": sig.as_bytes().iter().map(|b| format!("{:02x}", b)).collect::<String>() }))
}
"get" => {
let hash_hex = params
.get("hash")
.and_then(|v| v.as_str())
.ok_or_else(|| "missing 'hash'".to_string())?;
let sig_bytes = aether_atom::hex_decode(hash_hex)
.ok_or_else(|| "invalid hex hash".to_string())?;
let sig = aether_atom::Fingerprint(sig_bytes);
match lattice.lookup(&sig, "global") {
Some(hit) => Ok(json!({
"hash": hash_hex,
"found": true,
"content": String::from_utf8_lossy(hit.value.as_ref()).to_string(),
})),
None => Ok(json!({ "hash": hash_hex, "found": false })),
}
}
other => Err(format!("unknown method: {}", other)),
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
match cli.cmd {
Cmd::Daemon { listen, data_dir } => {
let dir = match data_dir {
Some(d) => std::path::PathBuf::from(d),
None => aether_atom::data_dir("AETHER_NET_DATA_DIR"),
};
let lattice = Arc::new(Mutex::new(Lattice::open(&dir)?));
eprintln!("aether-net-mini daemon listening on {} (data: {:?})", listen, dir);
let listener = ATcpListener::bind(&listen).await?;
loop {
let (stream, addr) = listener.accept().await?;
let lat = lattice.clone();
tokio::spawn(async move {
if let Err(e) = handle_conn(stream, lat).await {
eprintln!(" conn {} error: {}", addr, e);
}
});
}
}
Cmd::Client { server, method, params } => {
let mut s = TcpStream::connect(&server)?;
let req = json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params });
let req_str = serde_json::to_string(&req)? + "\n";
s.write_all(req_str.as_bytes())?;
let mut reader = BufReader::new(s);
let mut line = String::new();
reader.read_line(&mut line)?;
println!("{}", line.trim());
}
}
Ok(())
}
async fn handle_conn(
stream: tokio::net::TcpStream,
lattice: Arc<Mutex<Lattice>>,
) -> Result<(), Box<dyn std::error::Error>> {
let (read_half, mut write_half) = stream.into_split();
let mut reader = ABufReader::new(read_half);
let mut line = String::new();
loop {
line.clear();
let n = reader.read_line(&mut line).await?;
if n == 0 {
break; // EOF
}
if line.trim().is_empty() {
continue;
}
let resp = match serde_json::from_str::<Value>(&line) {
Ok(msg) => {
let id = msg.get("id").cloned().unwrap_or(Value::Null);
let method = msg.get("method").and_then(|m| m.as_str()).unwrap_or("");
let params = msg.get("params").cloned().unwrap_or_else(|| json!({}));
let mut lat = lattice.lock().await;
match dispatch(method, params, &mut lat).await {
Ok(result) => ok_response(id, result),
Err(msg) => err_response(id, -32000, &msg),
}
}
Err(_) => continue, // skip malformed lines
};
let s = serde_json::to_string(&resp)? + "\n";
write_half.write_all(s.as_bytes()).await?;
write_half.flush().await?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn put_then_get_round_trip() {
let dir = tempdir().unwrap();
let mut lat = Lattice::open(dir.path()).unwrap();
let v = dispatch(
"put",
json!({ "data": "hello world" }),
&mut lat,
)
.await
.unwrap();
let hash = v["hash"].as_str().unwrap().to_string();
let v = dispatch(
"get",
json!({ "hash": hash }),
&mut lat,
)
.await
.unwrap();
assert_eq!(v["found"], true);
assert_eq!(v["content"], "hello world");
}
#[tokio::test]
async fn get_unknown_returns_not_found() {
let dir = tempdir().unwrap();
let mut lat = Lattice::open(dir.path()).unwrap();
let v = dispatch(
"get",
json!({ "hash": "0000000000000000000000000000000000000000000000000000000000000000" }),
&mut lat,
)
.await
.unwrap();
assert_eq!(v["found"], false);
}
#[tokio::test]
async fn put_missing_data_errors() {
let dir = tempdir().unwrap();
let mut lat = Lattice::open(dir.path()).unwrap();
let r = dispatch("put", json!({}), &mut lat).await;
assert!(r.is_err());
assert!(r.unwrap_err().contains("data"));
}
#[tokio::test]
async fn get_invalid_hex_errors() {
let dir = tempdir().unwrap();
let mut lat = Lattice::open(dir.path()).unwrap();
let r = dispatch("get", json!({ "hash": "Z" }), &mut lat).await;
assert!(r.is_err());
}
#[tokio::test]
async fn unknown_method_errors() {
let dir = tempdir().unwrap();
let mut lat = Lattice::open(dir.path()).unwrap();
let r = dispatch("foo", json!({}), &mut lat).await;
assert!(r.is_err());
}
}