Compare commits
8 Commits
f8dd304820
...
refactor
Author | SHA1 | Date | |
---|---|---|---|
ee163bb6bf | |||
84611dd245 | |||
200d0c928d | |||
30a09e6d53 | |||
542996a0ff | |||
63ab39b4b1 | |||
ee94d731d7 | |||
c7945624bd |
1164
Cargo.lock
generated
1164
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
51
Cargo.toml
51
Cargo.toml
@@ -1,23 +1,38 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "redis-rs"
|
resolver = "2"
|
||||||
version = "0.0.1"
|
members = [
|
||||||
authors = ["Pin Fang <fpfangpin@hotmail.com>"]
|
"crates/herodb",
|
||||||
edition = "2021"
|
"crates/libdbstorage",
|
||||||
|
"crates/libcrypto",
|
||||||
|
"crates/libcryptoa",
|
||||||
|
"crates/herocrypto",
|
||||||
|
"crates/supervisor",
|
||||||
|
"crates/supervisorrpc",
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1.0.59"
|
# Common
|
||||||
bytes = "1.3.0"
|
anyhow = "1.0"
|
||||||
thiserror = "1.0.32"
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio = { version = "1.23.0", features = ["full"] }
|
|
||||||
clap = { version = "4.5.20", features = ["derive"] }
|
|
||||||
byteorder = "1.4.3"
|
|
||||||
futures = "0.3"
|
|
||||||
redb = "2.1.3"
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
bincode = "1.3.3"
|
serde_json = "1.0"
|
||||||
chacha20poly1305 = "0.10.1"
|
thiserror = "1.0"
|
||||||
|
log = "0.4"
|
||||||
|
bytes = "1.3"
|
||||||
|
|
||||||
|
# Crypto - Asymmetric
|
||||||
|
age = "0.10"
|
||||||
|
secrecy = "0.8"
|
||||||
|
ed25519-dalek = "2"
|
||||||
|
base64 = "0.22"
|
||||||
|
|
||||||
|
# Crypto - Symmetric & Utilities
|
||||||
|
chacha20poly1305 = "0.10"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
|
|
||||||
[dev-dependencies]
|
# Database
|
||||||
redis = { version = "0.24", features = ["aio", "tokio-comp"] }
|
redb = "2.1"
|
||||||
|
|
||||||
|
# CLI
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
10
crates/herocrypto/Cargo.toml
Normal file
10
crates/herocrypto/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "herocrypto"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
redis = { version = "0.24", features = ["tokio-comp"] }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
libcrypto = { path = "../libcrypto" }
|
||||||
|
libcryptoa = { path = "../libcryptoa" }
|
45
crates/herocrypto/src/lib.rs
Normal file
45
crates/herocrypto/src/lib.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// In crates/herocrypto/src/lib.rs
|
||||||
|
use redis::{Commands, RedisResult};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Redis connection error: {0}")]
|
||||||
|
Redis(#[from] redis::RedisError),
|
||||||
|
#[error("Asymmetric crypto error: {0}")]
|
||||||
|
Asymmetric(#[from] libcryptoa::AsymmetricCryptoError),
|
||||||
|
#[error("Key not found in database: {0}")]
|
||||||
|
KeyNotFound(String),
|
||||||
|
#[error("Command failed on server: {0}")]
|
||||||
|
CommandError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HeroCrypto {
|
||||||
|
// e.g., using a connection manager from redis-rs
|
||||||
|
client: redis::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeroCrypto {
|
||||||
|
pub fn new(redis_url: &str) -> Result<Self, Error> {
|
||||||
|
Ok(Self { client: redis::Client::open(redis_url)? })
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- High-level functions to be implemented ---
|
||||||
|
|
||||||
|
/// Generates a new keypair and stores it in HeroDB under the given name.
|
||||||
|
pub async fn generate_keypair(&self, name: &str) -> Result<(), Error> {
|
||||||
|
let mut con = self.client.get_async_connection().await?;
|
||||||
|
let (_pub, _priv): (String, String) = redis::cmd("AGE")
|
||||||
|
.arg("KEYGEN")
|
||||||
|
.arg(name)
|
||||||
|
.query_async(&mut con)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypts a message using a key stored in HeroDB.
|
||||||
|
pub async fn encrypt_by_name(&self, key_name: &str, plaintext: &str) -> Result<String, Error> {
|
||||||
|
// Implementation will call 'AGE ENCRYPTNAME ...'
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
31
crates/herodb/Cargo.toml
Normal file
31
crates/herodb/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
[package]
|
||||||
|
name = "herodb"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["Pin Fang <fpfangpin@hotmail.com>"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "herodb"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Workspace dependencies
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
log = { workspace = true }
|
||||||
|
clap = { workspace = true }
|
||||||
|
bytes = { workspace = true }
|
||||||
|
base64 = { workspace = true }
|
||||||
|
age = { workspace = true }
|
||||||
|
secrecy = { workspace = true }
|
||||||
|
ed25519-dalek = { workspace = true }
|
||||||
|
rand = { workspace = true }
|
||||||
|
|
||||||
|
# Local Crate Dependencies
|
||||||
|
libdbstorage = { path = "../libdbstorage" }
|
||||||
|
# We will create these libraries in the next steps
|
||||||
|
libcryptoa = { path = "../libcryptoa" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
redis = { version = "0.24", features = ["aio", "tokio-comp"] }
|
326
crates/herodb/age.rs
Normal file
326
crates/herodb/age.rs
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
//! age.rs — AGE (rage) helpers + persistent key management for your mini-Redis.
|
||||||
|
//
|
||||||
|
// Features:
|
||||||
|
// - X25519 encryption/decryption (age style)
|
||||||
|
// - Ed25519 detached signatures + verification
|
||||||
|
// - Persistent named keys in DB (strings):
|
||||||
|
// age:key:{name} -> X25519 recipient (public encryption key, "age1...")
|
||||||
|
// age:privkey:{name} -> X25519 identity (secret encryption key, "AGE-SECRET-KEY-1...")
|
||||||
|
// age:signpub:{name} -> Ed25519 verify pubkey (public, used to verify signatures)
|
||||||
|
// age:signpriv:{name} -> Ed25519 signing secret key (private, used to sign)
|
||||||
|
// - Base64 wrapping for ciphertext/signature binary blobs.
|
||||||
|
|
||||||
|
use crate::protocol::Protocol;
|
||||||
|
use crate::server::Server;
|
||||||
|
use libdbstorage::DBError;
|
||||||
|
use libcryptoa::AsymmetricCryptoError;
|
||||||
|
|
||||||
|
// ---------- Storage helpers ----------
|
||||||
|
|
||||||
|
fn sget(server: &Server, key: &str) -> Result<Option<String>, DBError> {
|
||||||
|
let st = server.current_storage()?;
|
||||||
|
st.get(key)
|
||||||
|
}
|
||||||
|
fn sset(server: &Server, key: &str, val: &str) -> Result<(), DBError> {
|
||||||
|
let st = server.current_storage()?;
|
||||||
|
st.set(key.to_string(), val.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enc_pub_key_key(name: &str) -> String { format!("age:key:{name}") }
|
||||||
|
fn enc_priv_key_key(name: &str) -> String { format!("age:privkey:{name}") }
|
||||||
|
fn sign_pub_key_key(name: &str) -> String { format!("age:signpub:{name}") }
|
||||||
|
fn sign_priv_key_key(name: &str) -> String { format!("age:signpriv:{name}") }
|
||||||
|
|
||||||
|
// ---------- Command handlers (RESP Protocol) ----------
|
||||||
|
// Basic (stateless) ones kept for completeness
|
||||||
|
|
||||||
|
pub async fn cmd_age_genenc() -> Protocol {
|
||||||
|
let (recip, ident) = libcryptoa::gen_enc_keypair();
|
||||||
|
Protocol::Array(vec![Protocol::BulkString(recip), Protocol::BulkString(ident)])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_gensign() -> Protocol {
|
||||||
|
let (verify, secret) = libcryptoa::gen_sign_keypair();
|
||||||
|
Protocol::Array(vec![Protocol::BulkString(verify), Protocol::BulkString(secret)])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_encrypt(recipient: &str, message: &str) -> Protocol {
|
||||||
|
match libcryptoa::encrypt_b64(recipient, message) {
|
||||||
|
Ok(b64) => Protocol::BulkString(b64),
|
||||||
|
Err(e) => Protocol::err(&format!("ERR age: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_decrypt(identity: &str, ct_b64: &str) -> Protocol {
|
||||||
|
match libcryptoa::decrypt_b64(identity, ct_b64) {
|
||||||
|
Ok(pt) => Protocol::BulkString(pt),
|
||||||
|
Err(e) => Protocol::err(&format!("ERR age: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_sign(secret: &str, message: &str) -> Protocol {
|
||||||
|
match libcryptoa::sign_b64(secret, message) {
|
||||||
|
Ok(b64sig) => Protocol::BulkString(b64sig),
|
||||||
|
Err(e) => Protocol::err(&format!("ERR age: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_verify(verify_pub: &str, message: &str, sig_b64: &str) -> Protocol {
|
||||||
|
match libcryptoa::verify_b64(verify_pub, message, sig_b64) {
|
||||||
|
Ok(true) => Protocol::SimpleString("1".to_string()),
|
||||||
|
Ok(false) => Protocol::SimpleString("0".to_string()),
|
||||||
|
Err(e) => Protocol::err(&format!("ERR age: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- NEW: Persistent, named-key commands ----------
|
||||||
|
|
||||||
|
pub async fn cmd_age_keygen(server: &Server, name: &str) -> Protocol {
|
||||||
|
let (recip, ident) = libcryptoa::gen_enc_keypair();
|
||||||
|
if let Err(e) = sset(server, &enc_pub_key_key(name), &recip) { return Protocol::err(&e.0); }
|
||||||
|
if let Err(e) = sset(server, &enc_priv_key_key(name), &ident) { return Protocol::err(&e.0); }
|
||||||
|
Protocol::Array(vec![Protocol::BulkString(recip), Protocol::BulkString(ident)])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_signkeygen(server: &Server, name: &str) -> Protocol {
|
||||||
|
let (verify, secret) = libcryptoa::gen_sign_keypair();
|
||||||
|
if let Err(e) = sset(server, &sign_pub_key_key(name), &verify) { return Protocol::err(&e.0); }
|
||||||
|
if let Err(e) = sset(server, &sign_priv_key_key(name), &secret) { return Protocol::err(&e.0); }
|
||||||
|
Protocol::Array(vec![Protocol::BulkString(verify), Protocol::BulkString(secret)])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_encrypt_name(server: &Server, name: &str, message: &str) -> Protocol {
|
||||||
|
let recip = match sget(server, &enc_pub_key_key(name)) {
|
||||||
|
Ok(Some(v)) => v,
|
||||||
|
Ok(None) => return Protocol::err(&format!("ERR age: missing recipient (age:key:{name})")),
|
||||||
|
Err(e) => return Protocol::err(&e.0),
|
||||||
|
};
|
||||||
|
match libcryptoa::encrypt_b64(&recip, message) {
|
||||||
|
Ok(ct) => Protocol::BulkString(ct),
|
||||||
|
Err(e) => Protocol::err(&format!("ERR age: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_decrypt_name(server: &Server, name: &str, ct_b64: &str) -> Protocol {
|
||||||
|
let ident = match sget(server, &enc_priv_key_key(name)) {
|
||||||
|
Ok(Some(v)) => v,
|
||||||
|
Ok(None) => return Protocol::err(&format!("ERR age: missing identity (age:privkey:{name})")),
|
||||||
|
Err(e) => return Protocol::err(&e.0),
|
||||||
|
};
|
||||||
|
match libcryptoa::decrypt_b64(&ident, ct_b64) {
|
||||||
|
Ok(pt) => Protocol::BulkString(pt),
|
||||||
|
Err(e) => Protocol::err(&format!("ERR age: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_sign_name(server: &Server, name: &str, message: &str) -> Protocol {
|
||||||
|
let sec = match sget(server, &sign_priv_key_key(name)) {
|
||||||
|
Ok(Some(v)) => v,
|
||||||
|
Ok(None) => return Protocol::err(&format!("ERR age: missing signing secret (age:signpriv:{name})")),
|
||||||
|
Err(e) => return Protocol::err(&e.0),
|
||||||
|
};
|
||||||
|
match libcryptoa::sign_b64(&sec, message) {
|
||||||
|
Ok(sig) => Protocol::BulkString(sig),
|
||||||
|
Err(e) => Protocol::err(&format!("ERR age: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_verify_name(server: &Server, name: &str, message: &str, sig_b64: &str) -> Protocol {
|
||||||
|
let pubk = match sget(server, &sign_pub_key_key(name)) {
|
||||||
|
Ok(Some(v)) => v,
|
||||||
|
Ok(None) => return Protocol::err(&format!("ERR age: missing verify pubkey (age:signpub:{name})")),
|
||||||
|
Err(e) => return Protocol::err(&e.0),
|
||||||
|
};
|
||||||
|
match libcryptoa::verify_b64(&pubk, message, sig_b64) {
|
||||||
|
Ok(true) => Protocol::SimpleString("1".to_string()),
|
||||||
|
Ok(false) => Protocol::SimpleString("0".to_string()),
|
||||||
|
Err(e) => Protocol::err(&format!("ERR age: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_list(server: &Server) -> Protocol {
|
||||||
|
// Returns 4 arrays: ["encpub", <names...>], ["encpriv", ...], ["signpub", ...], ["signpriv", ...]
|
||||||
|
let st = match server.current_storage() { Ok(s) => s, Err(e) => return Protocol::err(&e.0) };
|
||||||
|
|
||||||
|
let pull = |pat: &str, prefix: &str| -> Result<Vec<String>, DBError> {
|
||||||
|
let keys = st.keys(pat)?;
|
||||||
|
let mut names: Vec<String> = keys.into_iter()
|
||||||
|
.filter_map(|k| k.strip_prefix(prefix).map(|x| x.to_string()))
|
||||||
|
.collect();
|
||||||
|
names.sort();
|
||||||
|
Ok(names)
|
||||||
|
};
|
||||||
|
|
||||||
|
let encpub = match pull("age:key:*", "age:key:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
|
||||||
|
let encpriv = match pull("age:privkey:*", "age:privkey:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
|
||||||
|
let signpub = match pull("age:signpub:*", "age:signpub:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
|
||||||
|
let signpriv= match pull("age:signpriv:*", "age:signpriv:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
|
||||||
|
|
||||||
|
let to_arr = |label: &str, v: Vec<String>| {
|
||||||
|
let mut out = vec![Protocol::BulkString(label.to_string())];
|
||||||
|
out.push(Protocol::Array(v.into_iter().map(Protocol::BulkString).collect()));
|
||||||
|
Protocol::Array(out)
|
||||||
|
};
|
||||||
|
|
||||||
|
Protocol::Array(vec![
|
||||||
|
to_arr("encpub", encpub),
|
||||||
|
to_arr("encpriv", encpriv),
|
||||||
|
to_arr("signpub", signpub),
|
||||||
|
to_arr("signpriv", signpriv),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Storage helpers ----------
|
||||||
|
|
||||||
|
fn sget(server: &Server, key: &str) -> Result<Option<String>, AgeWireError> {
|
||||||
|
let st = server.current_storage().map_err(|e| AgeWireError::Storage(e.0))?;
|
||||||
|
st.get(key).map_err(|e| AgeWireError::Storage(e.0))
|
||||||
|
}
|
||||||
|
fn sset(server: &Server, key: &str, val: &str) -> Result<(), AgeWireError> {
|
||||||
|
let st = server.current_storage().map_err(|e| AgeWireError::Storage(e.0))?;
|
||||||
|
st.set(key.to_string(), val.to_string()).map_err(|e| AgeWireError::Storage(e.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enc_pub_key_key(name: &str) -> String { format!("age:key:{name}") }
|
||||||
|
fn enc_priv_key_key(name: &str) -> String { format!("age:privkey:{name}") }
|
||||||
|
fn sign_pub_key_key(name: &str) -> String { format!("age:signpub:{name}") }
|
||||||
|
fn sign_priv_key_key(name: &str) -> String { format!("age:signpriv:{name}") }
|
||||||
|
|
||||||
|
// ---------- Command handlers (RESP Protocol) ----------
|
||||||
|
// Basic (stateless) ones kept for completeness
|
||||||
|
|
||||||
|
pub async fn cmd_age_genenc() -> Protocol {
|
||||||
|
let (recip, ident) = gen_enc_keypair();
|
||||||
|
Protocol::Array(vec![Protocol::BulkString(recip), Protocol::BulkString(ident)])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_gensign() -> Protocol {
|
||||||
|
let (verify, secret) = gen_sign_keypair();
|
||||||
|
Protocol::Array(vec![Protocol::BulkString(verify), Protocol::BulkString(secret)])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_encrypt(recipient: &str, message: &str) -> Protocol {
|
||||||
|
match encrypt_b64(recipient, message) {
|
||||||
|
Ok(b64) => Protocol::BulkString(b64),
|
||||||
|
Err(e) => e.to_protocol(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_decrypt(identity: &str, ct_b64: &str) -> Protocol {
|
||||||
|
match decrypt_b64(identity, ct_b64) {
|
||||||
|
Ok(pt) => Protocol::BulkString(pt),
|
||||||
|
Err(e) => e.to_protocol(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_sign(secret: &str, message: &str) -> Protocol {
|
||||||
|
match sign_b64(secret, message) {
|
||||||
|
Ok(b64sig) => Protocol::BulkString(b64sig),
|
||||||
|
Err(e) => e.to_protocol(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_verify(verify_pub: &str, message: &str, sig_b64: &str) -> Protocol {
|
||||||
|
match verify_b64(verify_pub, message, sig_b64) {
|
||||||
|
Ok(true) => Protocol::SimpleString("1".to_string()),
|
||||||
|
Ok(false) => Protocol::SimpleString("0".to_string()),
|
||||||
|
Err(e) => e.to_protocol(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- NEW: Persistent, named-key commands ----------
|
||||||
|
|
||||||
|
pub async fn cmd_age_keygen(server: &Server, name: &str) -> Protocol {
|
||||||
|
let (recip, ident) = gen_enc_keypair();
|
||||||
|
if let Err(e) = sset(server, &enc_pub_key_key(name), &recip) { return e.to_protocol(); }
|
||||||
|
if let Err(e) = sset(server, &enc_priv_key_key(name), &ident) { return e.to_protocol(); }
|
||||||
|
Protocol::Array(vec![Protocol::BulkString(recip), Protocol::BulkString(ident)])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_signkeygen(server: &Server, name: &str) -> Protocol {
|
||||||
|
let (verify, secret) = gen_sign_keypair();
|
||||||
|
if let Err(e) = sset(server, &sign_pub_key_key(name), &verify) { return e.to_protocol(); }
|
||||||
|
if let Err(e) = sset(server, &sign_priv_key_key(name), &secret) { return e.to_protocol(); }
|
||||||
|
Protocol::Array(vec![Protocol::BulkString(verify), Protocol::BulkString(secret)])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_encrypt_name(server: &Server, name: &str, message: &str) -> Protocol {
|
||||||
|
let recip = match sget(server, &enc_pub_key_key(name)) {
|
||||||
|
Ok(Some(v)) => v,
|
||||||
|
Ok(None) => return AgeWireError::NotFound("recipient (age:key:{name})").to_protocol(),
|
||||||
|
Err(e) => return e.to_protocol(),
|
||||||
|
};
|
||||||
|
match encrypt_b64(&recip, message) {
|
||||||
|
Ok(ct) => Protocol::BulkString(ct),
|
||||||
|
Err(e) => e.to_protocol(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_decrypt_name(server: &Server, name: &str, ct_b64: &str) -> Protocol {
|
||||||
|
let ident = match sget(server, &enc_priv_key_key(name)) {
|
||||||
|
Ok(Some(v)) => v,
|
||||||
|
Ok(None) => return AgeWireError::NotFound("identity (age:privkey:{name})").to_protocol(),
|
||||||
|
Err(e) => return e.to_protocol(),
|
||||||
|
};
|
||||||
|
match decrypt_b64(&ident, ct_b64) {
|
||||||
|
Ok(pt) => Protocol::BulkString(pt),
|
||||||
|
Err(e) => e.to_protocol(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_sign_name(server: &Server, name: &str, message: &str) -> Protocol {
|
||||||
|
let sec = match sget(server, &sign_priv_key_key(name)) {
|
||||||
|
Ok(Some(v)) => v,
|
||||||
|
Ok(None) => return AgeWireError::NotFound("signing secret (age:signpriv:{name})").to_protocol(),
|
||||||
|
Err(e) => return e.to_protocol(),
|
||||||
|
};
|
||||||
|
match sign_b64(&sec, message) {
|
||||||
|
Ok(sig) => Protocol::BulkString(sig),
|
||||||
|
Err(e) => e.to_protocol(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_verify_name(server: &Server, name: &str, message: &str, sig_b64: &str) -> Protocol {
|
||||||
|
let pubk = match sget(server, &sign_pub_key_key(name)) {
|
||||||
|
Ok(Some(v)) => v,
|
||||||
|
Ok(None) => return AgeWireError::NotFound("verify pubkey (age:signpub:{name})").to_protocol(),
|
||||||
|
Err(e) => return e.to_protocol(),
|
||||||
|
};
|
||||||
|
match verify_b64(&pubk, message, sig_b64) {
|
||||||
|
Ok(true) => Protocol::SimpleString("1".to_string()),
|
||||||
|
Ok(false) => Protocol::SimpleString("0".to_string()),
|
||||||
|
Err(e) => e.to_protocol(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_age_list(server: &Server) -> Protocol {
|
||||||
|
// Returns 4 arrays: ["encpub", <names...>], ["encpriv", ...], ["signpub", ...], ["signpriv", ...]
|
||||||
|
let st = match server.current_storage() { Ok(s) => s, Err(e) => return Protocol::err(&e.0) };
|
||||||
|
|
||||||
|
let pull = |pat: &str, prefix: &str| -> Result<Vec<String>, DBError> {
|
||||||
|
let keys = st.keys(pat)?;
|
||||||
|
let mut names: Vec<String> = keys.into_iter()
|
||||||
|
.filter_map(|k| k.strip_prefix(prefix).map(|x| x.to_string()))
|
||||||
|
.collect();
|
||||||
|
names.sort();
|
||||||
|
Ok(names)
|
||||||
|
};
|
||||||
|
|
||||||
|
let encpub = match pull("age:key:*", "age:key:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
|
||||||
|
let encpriv = match pull("age:privkey:*", "age:privkey:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
|
||||||
|
let signpub = match pull("age:signpub:*", "age:signpub:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
|
||||||
|
let signpriv= match pull("age:signpriv:*", "age:signpriv:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
|
||||||
|
|
||||||
|
let to_arr = |label: &str, v: Vec<String>| {
|
||||||
|
let mut out = vec![Protocol::BulkString(label.to_string())];
|
||||||
|
out.push(Protocol::Array(v.into_iter().map(Protocol::BulkString).collect()));
|
||||||
|
Protocol::Array(out)
|
||||||
|
};
|
||||||
|
|
||||||
|
Protocol::Array(vec![
|
||||||
|
to_arr("encpub", encpub),
|
||||||
|
to_arr("encpriv", encpriv),
|
||||||
|
to_arr("signpub", signpub),
|
||||||
|
to_arr("signpriv", signpriv),
|
||||||
|
])
|
||||||
|
}
|
@@ -1,4 +1,7 @@
|
|||||||
use crate::{error::DBError, protocol::Protocol, server::Server};
|
use crate::protocol::Protocol;
|
||||||
|
use crate::server::Server;
|
||||||
|
use libdbstorage::DBError;
|
||||||
|
use libcryptoa;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -50,6 +53,22 @@ pub enum Cmd {
|
|||||||
LRange(String, i64, i64),
|
LRange(String, i64, i64),
|
||||||
FlushDb,
|
FlushDb,
|
||||||
Unknow(String),
|
Unknow(String),
|
||||||
|
// AGE (rage) commands — stateless
|
||||||
|
AgeGenEnc,
|
||||||
|
AgeGenSign,
|
||||||
|
AgeEncrypt(String, String), // recipient, message
|
||||||
|
AgeDecrypt(String, String), // identity, ciphertext_b64
|
||||||
|
AgeSign(String, String), // signing_secret, message
|
||||||
|
AgeVerify(String, String, String), // verify_pub, message, signature_b64
|
||||||
|
|
||||||
|
// NEW: persistent named-key commands
|
||||||
|
AgeKeygen(String), // name
|
||||||
|
AgeSignKeygen(String), // name
|
||||||
|
AgeEncryptName(String, String), // name, message
|
||||||
|
AgeDecryptName(String, String), // name, ciphertext_b64
|
||||||
|
AgeSignName(String, String), // name, message
|
||||||
|
AgeVerifyName(String, String, String), // name, message, signature_b64
|
||||||
|
AgeList,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cmd {
|
impl Cmd {
|
||||||
@@ -402,6 +421,43 @@ impl Cmd {
|
|||||||
}
|
}
|
||||||
Cmd::FlushDb
|
Cmd::FlushDb
|
||||||
}
|
}
|
||||||
|
"age" => {
|
||||||
|
if cmd.len() < 2 {
|
||||||
|
return Err(DBError("wrong number of arguments for AGE".to_string()));
|
||||||
|
}
|
||||||
|
match cmd[1].to_lowercase().as_str() {
|
||||||
|
// stateless
|
||||||
|
"genenc" => { if cmd.len() != 2 { return Err(DBError("AGE GENENC takes no args".to_string())); }
|
||||||
|
Cmd::AgeGenEnc }
|
||||||
|
"gensign" => { if cmd.len() != 2 { return Err(DBError("AGE GENSIGN takes no args".to_string())); }
|
||||||
|
Cmd::AgeGenSign }
|
||||||
|
"encrypt" => { if cmd.len() != 4 { return Err(DBError("AGE ENCRYPT <recipient> <message>".to_string())); }
|
||||||
|
Cmd::AgeEncrypt(cmd[2].clone(), cmd[3].clone()) }
|
||||||
|
"decrypt" => { if cmd.len() != 4 { return Err(DBError("AGE DECRYPT <identity> <ciphertext_b64>".to_string())); }
|
||||||
|
Cmd::AgeDecrypt(cmd[2].clone(), cmd[3].clone()) }
|
||||||
|
"sign" => { if cmd.len() != 4 { return Err(DBError("AGE SIGN <signing_secret> <message>".to_string())); }
|
||||||
|
Cmd::AgeSign(cmd[2].clone(), cmd[3].clone()) }
|
||||||
|
"verify" => { if cmd.len() != 5 { return Err(DBError("AGE VERIFY <verify_pub> <message> <signature_b64>".to_string())); }
|
||||||
|
Cmd::AgeVerify(cmd[2].clone(), cmd[3].clone(), cmd[4].clone()) }
|
||||||
|
|
||||||
|
// persistent names
|
||||||
|
"keygen" => { if cmd.len() != 3 { return Err(DBError("AGE KEYGEN <name>".to_string())); }
|
||||||
|
Cmd::AgeKeygen(cmd[2].clone()) }
|
||||||
|
"signkeygen" => { if cmd.len() != 3 { return Err(DBError("AGE SIGNKEYGEN <name>".to_string())); }
|
||||||
|
Cmd::AgeSignKeygen(cmd[2].clone()) }
|
||||||
|
"encryptname" => { if cmd.len() != 4 { return Err(DBError("AGE ENCRYPTNAME <name> <message>".to_string())); }
|
||||||
|
Cmd::AgeEncryptName(cmd[2].clone(), cmd[3].clone()) }
|
||||||
|
"decryptname" => { if cmd.len() != 4 { return Err(DBError("AGE DECRYPTNAME <name> <ciphertext_b64>".to_string())); }
|
||||||
|
Cmd::AgeDecryptName(cmd[2].clone(), cmd[3].clone()) }
|
||||||
|
"signname" => { if cmd.len() != 4 { return Err(DBError("AGE SIGNNAME <name> <message>".to_string())); }
|
||||||
|
Cmd::AgeSignName(cmd[2].clone(), cmd[3].clone()) }
|
||||||
|
"verifyname" => { if cmd.len() != 5 { return Err(DBError("AGE VERIFYNAME <name> <message> <signature_b64>".to_string())); }
|
||||||
|
Cmd::AgeVerifyName(cmd[2].clone(), cmd[3].clone(), cmd[4].clone()) }
|
||||||
|
"list" => { if cmd.len() != 2 { return Err(DBError("AGE LIST".to_string())); }
|
||||||
|
Cmd::AgeList }
|
||||||
|
_ => return Err(DBError(format!("unsupported AGE subcommand {:?}", cmd))),
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => Cmd::Unknow(cmd[0].clone()),
|
_ => Cmd::Unknow(cmd[0].clone()),
|
||||||
},
|
},
|
||||||
protocol,
|
protocol,
|
||||||
@@ -484,6 +540,22 @@ impl Cmd {
|
|||||||
Cmd::LIndex(key, index) => lindex_cmd(server, &key, index).await,
|
Cmd::LIndex(key, index) => lindex_cmd(server, &key, index).await,
|
||||||
Cmd::LRange(key, start, stop) => lrange_cmd(server, &key, start, stop).await,
|
Cmd::LRange(key, start, stop) => lrange_cmd(server, &key, start, stop).await,
|
||||||
Cmd::FlushDb => flushdb_cmd(server).await,
|
Cmd::FlushDb => flushdb_cmd(server).await,
|
||||||
|
// AGE (rage): stateless
|
||||||
|
Cmd::AgeGenEnc => Ok(libcryptoa::gen_enc_keypair().await),
|
||||||
|
Cmd::AgeGenSign => Ok(libcryptoa::gen_sign_keypair().await),
|
||||||
|
Cmd::AgeEncrypt(recipient, message) => Ok(libcryptoa::encrypt_b64(&recipient, &message).await),
|
||||||
|
Cmd::AgeDecrypt(identity, ct_b64) => Ok(libcryptoa::decrypt_b64(&identity, &ct_b64).await),
|
||||||
|
Cmd::AgeSign(secret, message) => Ok(libcryptoa::sign_b64(&secret, &message).await),
|
||||||
|
Cmd::AgeVerify(vpub, msg, sig_b64) => Ok(libcryptoa::verify_b64(&vpub, &msg, &sig_b64).await),
|
||||||
|
|
||||||
|
// AGE (rage): persistent named keys
|
||||||
|
Cmd::AgeKeygen(name) => Ok(crate::age::cmd_age_keygen(server, &name).await),
|
||||||
|
Cmd::AgeSignKeygen(name) => Ok(crate::age::cmd_age_signkeygen(server, &name).await),
|
||||||
|
Cmd::AgeEncryptName(name, message) => Ok(crate::age::cmd_age_encrypt_name(server, &name, &message).await),
|
||||||
|
Cmd::AgeDecryptName(name, ct_b64) => Ok(crate::age::cmd_age_decrypt_name(server, &name, &ct_b64).await),
|
||||||
|
Cmd::AgeSignName(name, message) => Ok(crate::age::cmd_age_sign_name(server, &name, &message).await),
|
||||||
|
Cmd::AgeVerifyName(name, message, sig_b64) => Ok(crate::age::cmd_age_verify_name(server, &name, &message, &sig_b64).await),
|
||||||
|
Cmd::AgeList => Ok(crate::age::cmd_age_list(server).await),
|
||||||
Cmd::Unknow(s) => Ok(Protocol::err(&format!("ERR unknown command `{}`", s))),
|
Cmd::Unknow(s) => Ok(Protocol::err(&format!("ERR unknown command `{}`", s))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -553,40 +625,40 @@ async fn llen_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn lpop_cmd(server: &Server, key: &str, count: &Option<u64>) -> Result<Protocol, DBError> {
|
async fn lpop_cmd(server: &Server, key: &str, count: &Option<u64>) -> Result<Protocol, DBError> {
|
||||||
match server.current_storage()?.lpop(key, *count) {
|
let count_val = count.unwrap_or(1);
|
||||||
Ok(Some(elements)) => {
|
match server.current_storage()?.lpop(key, count_val) {
|
||||||
if count.is_some() {
|
Ok(elements) => {
|
||||||
Ok(Protocol::Array(elements.into_iter().map(Protocol::BulkString).collect()))
|
if elements.is_empty() {
|
||||||
} else {
|
|
||||||
Ok(Protocol::BulkString(elements[0].clone()))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Ok(None) => {
|
|
||||||
if count.is_some() {
|
if count.is_some() {
|
||||||
Ok(Protocol::Array(vec![]))
|
Ok(Protocol::Array(vec![]))
|
||||||
} else {
|
} else {
|
||||||
Ok(Protocol::Null)
|
Ok(Protocol::Null)
|
||||||
}
|
}
|
||||||
|
} else if count.is_some() {
|
||||||
|
Ok(Protocol::Array(elements.into_iter().map(Protocol::BulkString).collect()))
|
||||||
|
} else {
|
||||||
|
Ok(Protocol::BulkString(elements[0].clone()))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Err(e) => Ok(Protocol::err(&e.0)),
|
Err(e) => Ok(Protocol::err(&e.0)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn rpop_cmd(server: &Server, key: &str, count: &Option<u64>) -> Result<Protocol, DBError> {
|
async fn rpop_cmd(server: &Server, key: &str, count: &Option<u64>) -> Result<Protocol, DBError> {
|
||||||
match server.current_storage()?.rpop(key, *count) {
|
let count_val = count.unwrap_or(1);
|
||||||
Ok(Some(elements)) => {
|
match server.current_storage()?.rpop(key, count_val) {
|
||||||
if count.is_some() {
|
Ok(elements) => {
|
||||||
Ok(Protocol::Array(elements.into_iter().map(Protocol::BulkString).collect()))
|
if elements.is_empty() {
|
||||||
} else {
|
|
||||||
Ok(Protocol::BulkString(elements[0].clone()))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Ok(None) => {
|
|
||||||
if count.is_some() {
|
if count.is_some() {
|
||||||
Ok(Protocol::Array(vec![]))
|
Ok(Protocol::Array(vec![]))
|
||||||
} else {
|
} else {
|
||||||
Ok(Protocol::Null)
|
Ok(Protocol::Null)
|
||||||
}
|
}
|
||||||
|
} else if count.is_some() {
|
||||||
|
Ok(Protocol::Array(elements.into_iter().map(Protocol::BulkString).collect()))
|
||||||
|
} else {
|
||||||
|
Ok(Protocol::BulkString(elements[0].clone()))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Err(e) => Ok(Protocol::err(&e.0)),
|
Err(e) => Ok(Protocol::err(&e.0)),
|
||||||
}
|
}
|
||||||
@@ -746,7 +818,7 @@ async fn get_cmd(server: &Server, k: &str) -> Result<Protocol, DBError> {
|
|||||||
|
|
||||||
// Hash command implementations
|
// Hash command implementations
|
||||||
async fn hset_cmd(server: &Server, key: &str, pairs: &[(String, String)]) -> Result<Protocol, DBError> {
|
async fn hset_cmd(server: &Server, key: &str, pairs: &[(String, String)]) -> Result<Protocol, DBError> {
|
||||||
let new_fields = server.current_storage()?.hset(key, pairs)?;
|
let new_fields = server.current_storage()?.hset(key, pairs.to_vec())?;
|
||||||
Ok(Protocol::SimpleString(new_fields.to_string()))
|
Ok(Protocol::SimpleString(new_fields.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,7 +845,7 @@ async fn hgetall_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn hdel_cmd(server: &Server, key: &str, fields: &[String]) -> Result<Protocol, DBError> {
|
async fn hdel_cmd(server: &Server, key: &str, fields: &[String]) -> Result<Protocol, DBError> {
|
||||||
match server.current_storage()?.hdel(key, fields) {
|
match server.current_storage()?.hdel(key, fields.to_vec()) {
|
||||||
Ok(deleted) => Ok(Protocol::SimpleString(deleted.to_string())),
|
Ok(deleted) => Ok(Protocol::SimpleString(deleted.to_string())),
|
||||||
Err(e) => Ok(Protocol::err(&e.0)),
|
Err(e) => Ok(Protocol::err(&e.0)),
|
||||||
}
|
}
|
||||||
@@ -812,7 +884,7 @@ async fn hlen_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn hmget_cmd(server: &Server, key: &str, fields: &[String]) -> Result<Protocol, DBError> {
|
async fn hmget_cmd(server: &Server, key: &str, fields: &[String]) -> Result<Protocol, DBError> {
|
||||||
match server.current_storage()?.hmget(key, fields) {
|
match server.current_storage()?.hmget(key, fields.to_vec()) {
|
||||||
Ok(values) => {
|
Ok(values) => {
|
||||||
let result: Vec<Protocol> = values
|
let result: Vec<Protocol> = values
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -838,10 +910,12 @@ async fn scan_cmd(
|
|||||||
count: &Option<u64>
|
count: &Option<u64>
|
||||||
) -> Result<Protocol, DBError> {
|
) -> Result<Protocol, DBError> {
|
||||||
match server.current_storage()?.scan(*cursor, pattern, *count) {
|
match server.current_storage()?.scan(*cursor, pattern, *count) {
|
||||||
Ok((next_cursor, keys)) => {
|
Ok((next_cursor, key_value_pairs)) => {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
result.push(Protocol::BulkString(next_cursor.to_string()));
|
result.push(Protocol::BulkString(next_cursor.to_string()));
|
||||||
result.push(Protocol::Array(keys.into_iter().map(Protocol::BulkString).collect()));
|
// For SCAN, we only return the keys, not the values
|
||||||
|
let keys: Vec<Protocol> = key_value_pairs.into_iter().map(|(key, _)| Protocol::BulkString(key)).collect();
|
||||||
|
result.push(Protocol::Array(keys));
|
||||||
Ok(Protocol::Array(result))
|
Ok(Protocol::Array(result))
|
||||||
}
|
}
|
||||||
Err(e) => Ok(Protocol::err(&format!("ERR {}", e.0))),
|
Err(e) => Ok(Protocol::err(&format!("ERR {}", e.0))),
|
||||||
@@ -856,10 +930,16 @@ async fn hscan_cmd(
|
|||||||
count: &Option<u64>
|
count: &Option<u64>
|
||||||
) -> Result<Protocol, DBError> {
|
) -> Result<Protocol, DBError> {
|
||||||
match server.current_storage()?.hscan(key, *cursor, pattern, *count) {
|
match server.current_storage()?.hscan(key, *cursor, pattern, *count) {
|
||||||
Ok((next_cursor, fields)) => {
|
Ok((next_cursor, field_value_pairs)) => {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
result.push(Protocol::BulkString(next_cursor.to_string()));
|
result.push(Protocol::BulkString(next_cursor.to_string()));
|
||||||
result.push(Protocol::Array(fields.into_iter().map(Protocol::BulkString).collect()));
|
// For HSCAN, we return field-value pairs flattened
|
||||||
|
let mut fields_and_values = Vec::new();
|
||||||
|
for (field, value) in field_value_pairs {
|
||||||
|
fields_and_values.push(Protocol::BulkString(field));
|
||||||
|
fields_and_values.push(Protocol::BulkString(value));
|
||||||
|
}
|
||||||
|
result.push(Protocol::Array(fields_and_values));
|
||||||
Ok(Protocol::Array(result))
|
Ok(Protocol::Array(result))
|
||||||
}
|
}
|
||||||
Err(e) => Ok(Protocol::err(&format!("ERR {}", e.0))),
|
Err(e) => Ok(Protocol::err(&format!("ERR {}", e.0))),
|
71
crates/herodb/examples/age_bash_demo.sh
Executable file
71
crates/herodb/examples/age_bash_demo.sh
Executable file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Start the herodb server in the background
|
||||||
|
echo "Starting herodb server..."
|
||||||
|
cargo run -p herodb -- --dir /tmp/herodb_age_test --port 6382 --debug --encryption-key "testkey" &
|
||||||
|
SERVER_PID=$!
|
||||||
|
sleep 2 # Give the server a moment to start
|
||||||
|
|
||||||
|
REDIS_CLI="redis-cli -p 6382"
|
||||||
|
|
||||||
|
echo "--- Generating and Storing Encryption Keys ---"
|
||||||
|
# The new AGE commands are 'AGE KEYGEN <name>' etc., based on src/cmd.rs
|
||||||
|
# This script uses older commands like 'AGE.GENERATE_KEYPAIR alice'
|
||||||
|
# The demo script needs to be updated to match the implemented commands.
|
||||||
|
# Let's assume the commands in the script are what's expected for now,
|
||||||
|
# but note this discrepancy. The new commands are AGE KEYGEN etc.
|
||||||
|
# The script here uses a different syntax not found in src/cmd.rs like 'AGE.GENERATE_KEYPAIR'.
|
||||||
|
# For now, I will modify the script to fit the actual implementation.
|
||||||
|
|
||||||
|
echo "--- Generating and Storing Encryption Keys ---"
|
||||||
|
$REDIS_CLI AGE KEYGEN alice
|
||||||
|
$REDIS_CLI AGE KEYGEN bob
|
||||||
|
|
||||||
|
echo "--- Encrypting and Decrypting a Message ---"
|
||||||
|
MESSAGE="Hello, AGE encryption!"
|
||||||
|
# The new logic stores keys internally and does not expose a command to get the public key.
|
||||||
|
# We will encrypt by name.
|
||||||
|
ALICE_PUBKEY_REPLY=$($REDIS_CLI AGE KEYGEN alice | head -n 2 | tail -n 1)
|
||||||
|
echo "Alice's Public Key: $ALICE_PUBKEY_REPLY"
|
||||||
|
|
||||||
|
echo "Encrypting message: '$MESSAGE' with Alice's identity..."
|
||||||
|
# AGE.ENCRYPT recipient message. But since we use persistent keys, let's use ENCRYPTNAME
|
||||||
|
CIPHERTEXT=$($REDIS_CLI AGE ENCRYPTNAME alice "$MESSAGE")
|
||||||
|
echo "Ciphertext: $CIPHERTEXT"
|
||||||
|
|
||||||
|
echo "Decrypting ciphertext with Alice's private key..."
|
||||||
|
DECRYPTED_MESSAGE=$($REDIS_CLI AGE DECRYPTNAME alice "$CIPHERTEXT")
|
||||||
|
echo "Decrypted Message: $DECRYPTED_MESSAGE"
|
||||||
|
|
||||||
|
echo "--- Generating and Storing Signing Keys ---"
|
||||||
|
$REDIS_CLI AGE SIGNKEYGEN signer1
|
||||||
|
|
||||||
|
echo "--- Signing and Verifying a Message ---"
|
||||||
|
SIGN_MESSAGE="This is a message to be signed."
|
||||||
|
# Similar to above, we don't have GET_SIGN_PUBKEY. We will verify by name.
|
||||||
|
|
||||||
|
echo "Signing message: '$SIGN_MESSAGE' with signer1's private key..."
|
||||||
|
SIGNATURE=$($REDIS_CLI AGE SIGNNAME "$SIGN_MESSAGE" signer1)
|
||||||
|
echo "Signature: $SIGNATURE"
|
||||||
|
|
||||||
|
echo "Verifying signature with signer1's public key..."
|
||||||
|
VERIFY_RESULT=$($REDIS_CLI AGE VERIFYNAME signer1 "$SIGN_MESSAGE" "$SIGNATURE")
|
||||||
|
echo "Verification Result: $VERIFY_RESULT"
|
||||||
|
|
||||||
|
|
||||||
|
# There is no DELETE_KEYPAIR command in the implementation
|
||||||
|
echo "--- Cleaning up keys (manual in herodb) ---"
|
||||||
|
# We would use DEL for age:key:alice, etc.
|
||||||
|
$REDIS_CLI DEL age:key:alice
|
||||||
|
$REDIS_CLI DEL age:privkey:alice
|
||||||
|
$REDIS_CLI DEL age:key:bob
|
||||||
|
$REDIS_CLI DEL age:privkey:bob
|
||||||
|
$REDIS_CLI DEL age:signpub:signer1
|
||||||
|
$REDIS_CLI DEL age:signpriv:signer1
|
||||||
|
|
||||||
|
echo "--- Stopping herodb server ---"
|
||||||
|
kill $SERVER_PID
|
||||||
|
wait $SERVER_PID 2>/dev/null
|
||||||
|
echo "Server stopped."
|
||||||
|
|
||||||
|
echo "Bash demo complete."
|
83
crates/herodb/examples/age_persist_demo.rs
Normal file
83
crates/herodb/examples/age_persist_demo.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::TcpStream;
|
||||||
|
|
||||||
|
// Minimal RESP helpers
|
||||||
|
fn arr(parts: &[&str]) -> String {
|
||||||
|
let mut out = format!("*{}\r\n", parts.len());
|
||||||
|
for p in parts {
|
||||||
|
out.push_str(&format!("${}\r\n{}\r\n", p.len(), p));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
fn read_reply(s: &mut TcpStream) -> String {
|
||||||
|
let mut buf = [0u8; 65536];
|
||||||
|
let n = s.read(&mut buf).unwrap();
|
||||||
|
String::from_utf8_lossy(&buf[..n]).to_string()
|
||||||
|
}
|
||||||
|
fn parse_two_bulk(reply: &str) -> Option<(String,String)> {
|
||||||
|
let mut lines = reply.split("\r\n");
|
||||||
|
if lines.next()? != "*2" { return None; }
|
||||||
|
let _n = lines.next()?;
|
||||||
|
let a = lines.next()?.to_string();
|
||||||
|
let _m = lines.next()?;
|
||||||
|
let b = lines.next()?.to_string();
|
||||||
|
Some((a,b))
|
||||||
|
}
|
||||||
|
fn parse_bulk(reply: &str) -> Option<String> {
|
||||||
|
let mut lines = reply.split("\r\n");
|
||||||
|
let hdr = lines.next()?;
|
||||||
|
if !hdr.starts_with('$') { return None; }
|
||||||
|
Some(lines.next()?.to_string())
|
||||||
|
}
|
||||||
|
fn parse_simple(reply: &str) -> Option<String> {
|
||||||
|
let mut lines = reply.split("\r\n");
|
||||||
|
let hdr = lines.next()?;
|
||||||
|
if !hdr.starts_with('+') { return None; }
|
||||||
|
Some(hdr[1..].to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
let host = args.next().unwrap_or_else(|| "127.0.0.1".into());
|
||||||
|
let port = args.next().unwrap_or_else(|| "6379".into());
|
||||||
|
let addr = format!("{host}:{port}");
|
||||||
|
println!("Connecting to {addr}...");
|
||||||
|
let mut s = TcpStream::connect(addr).expect("connect");
|
||||||
|
|
||||||
|
// Generate & persist X25519 enc keys under name "alice"
|
||||||
|
s.write_all(arr(&["age","keygen","alice"]).as_bytes()).unwrap();
|
||||||
|
let (_alice_recip, _alice_ident) = parse_two_bulk(&read_reply(&mut s)).expect("gen enc");
|
||||||
|
|
||||||
|
// Generate & persist Ed25519 signing key under name "signer"
|
||||||
|
s.write_all(arr(&["age","signkeygen","signer"]).as_bytes()).unwrap();
|
||||||
|
let (_verify, _secret) = parse_two_bulk(&read_reply(&mut s)).expect("gen sign");
|
||||||
|
|
||||||
|
// Encrypt by name
|
||||||
|
let msg = "hello from persistent keys";
|
||||||
|
s.write_all(arr(&["age","encryptname","alice", msg]).as_bytes()).unwrap();
|
||||||
|
let ct_b64 = parse_bulk(&read_reply(&mut s)).expect("ct b64");
|
||||||
|
println!("ciphertext b64: {}", ct_b64);
|
||||||
|
|
||||||
|
// Decrypt by name
|
||||||
|
s.write_all(arr(&["age","decryptname","alice", &ct_b64]).as_bytes()).unwrap();
|
||||||
|
let pt = parse_bulk(&read_reply(&mut s)).expect("pt");
|
||||||
|
assert_eq!(pt, msg);
|
||||||
|
println!("decrypted ok");
|
||||||
|
|
||||||
|
// Sign by name
|
||||||
|
s.write_all(arr(&["age","signname","signer", msg]).as_bytes()).unwrap();
|
||||||
|
let sig_b64 = parse_bulk(&read_reply(&mut s)).expect("sig b64");
|
||||||
|
|
||||||
|
// Verify by name
|
||||||
|
s.write_all(arr(&["age","verifyname","signer", msg, &sig_b64]).as_bytes()).unwrap();
|
||||||
|
let ok = parse_simple(&read_reply(&mut s)).expect("verify");
|
||||||
|
assert_eq!(ok, "1");
|
||||||
|
println!("signature verified");
|
||||||
|
|
||||||
|
// List names
|
||||||
|
s.write_all(arr(&["age","list"]).as_bytes()).unwrap();
|
||||||
|
let list = read_reply(&mut s);
|
||||||
|
println!("LIST -> {list}");
|
||||||
|
|
||||||
|
println!("✔ persistent AGE workflow complete.");
|
||||||
|
}
|
4
crates/herodb/lib.rs
Normal file
4
crates/herodb/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod age; // NEW
|
||||||
|
pub mod cmd;
|
||||||
|
pub mod protocol;
|
||||||
|
pub mod server;
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
use redis_rs::server;
|
use herodb::server;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ async fn main() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// new DB option
|
// new DB option
|
||||||
let option = redis_rs::options::DBOption {
|
let option = herodb::options::DBOption {
|
||||||
dir: args.dir,
|
dir: args.dir,
|
||||||
port,
|
port,
|
||||||
debug: args.debug,
|
debug: args.debug,
|
@@ -30,7 +30,7 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_storage(&self) -> Result<Arc<Storage>, DBError> {
|
pub fn current_storage(&self) -> Result<Arc<libdbstorage::Storage>, libdbstorage::DBError> {
|
||||||
let mut cache = self.db_cache.write().unwrap();
|
let mut cache = self.db_cache.write().unwrap();
|
||||||
|
|
||||||
if let Some(storage) = cache.get(&self.selected_db) {
|
if let Some(storage) = cache.get(&self.selected_db) {
|
||||||
@@ -61,8 +61,9 @@ impl Server {
|
|||||||
Ok(storage)
|
Ok(storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_encrypt_db(&self, _db_index: u64) -> bool {
|
fn should_encrypt_db(&self, db_index: u64) -> bool {
|
||||||
self.option.encrypt
|
// DB 0-9 are non-encrypted, DB 10+ are encrypted
|
||||||
|
self.option.encrypt && db_index >= 10
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle(
|
pub async fn handle(
|
1
crates/herodb/src/main.rs
Normal file
1
crates/herodb/src/main.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
fn main() {}
|
@@ -1,4 +1,4 @@
|
|||||||
use redis_rs::{server::Server, options::DBOption};
|
use herodb::{server::Server, options::DBOption};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
@@ -1,4 +1,4 @@
|
|||||||
use redis_rs::{server::Server, options::DBOption};
|
use herodb::{server::Server, options::DBOption};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
@@ -1,5 +1,5 @@
|
|||||||
use redis_rs::protocol::Protocol;
|
use herodb::protocol::Protocol;
|
||||||
use redis_rs::cmd::Cmd;
|
use herodb::cmd::Cmd;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_protocol_parsing() {
|
fn test_protocol_parsing() {
|
@@ -1,4 +1,4 @@
|
|||||||
use redis_rs::{server::Server, options::DBOption};
|
use herodb::{server::Server, options::DBOption};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
@@ -1,4 +1,4 @@
|
|||||||
use redis_rs::{server::Server, options::DBOption};
|
use herodb::{server::Server, options::DBOption};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
@@ -1,4 +1,4 @@
|
|||||||
use redis_rs::{server::Server, options::DBOption};
|
use herodb::{server::Server, options::DBOption};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
10
crates/libcrypto/Cargo.toml
Normal file
10
crates/libcrypto/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "libcrypto"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chacha20poly1305 = { workspace = true }
|
||||||
|
rand = { workspace = true }
|
||||||
|
sha2 = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
@@ -1,25 +1,24 @@
|
|||||||
|
// In crates/libcrypto/src/lib.rs
|
||||||
use chacha20poly1305::{
|
use chacha20poly1305::{
|
||||||
aead::{Aead, KeyInit, OsRng},
|
aead::{Aead, KeyInit, OsRng},
|
||||||
XChaCha20Poly1305, XNonce,
|
XChaCha20Poly1305, XNonce,
|
||||||
};
|
};
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
const VERSION: u8 = 1;
|
const VERSION: u8 = 1;
|
||||||
const NONCE_LEN: usize = 24;
|
const NONCE_LEN: usize = 24;
|
||||||
const TAG_LEN: usize = 16;
|
const TAG_LEN: usize = 16;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum CryptoError {
|
pub enum CryptoError {
|
||||||
Format, // wrong length / header
|
#[error("invalid format: data too short")]
|
||||||
Version(u8), // unknown version
|
Format,
|
||||||
Decrypt, // wrong key or corrupted data
|
#[error("unknown version: {0}")]
|
||||||
}
|
Version(u8),
|
||||||
|
#[error("decryption failed: wrong key or corrupted data")]
|
||||||
impl From<CryptoError> for crate::error::DBError {
|
Decrypt,
|
||||||
fn from(e: CryptoError) -> Self {
|
|
||||||
crate::error::DBError(format!("Crypto error: {:?}", e))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Super-simple factory: new(secret) + encrypt(bytes) + decrypt(bytes)
|
/// Super-simple factory: new(secret) + encrypt(bytes) + decrypt(bytes)
|
12
crates/libcryptoa/Cargo.toml
Normal file
12
crates/libcryptoa/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "libcryptoa"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
age = { workspace = true }
|
||||||
|
secrecy = { workspace = true }
|
||||||
|
ed25519-dalek = { workspace = true }
|
||||||
|
base64 = { workspace = true }
|
||||||
|
rand = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
100
crates/libcryptoa/src/lib.rs
Normal file
100
crates/libcryptoa/src/lib.rs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// In crates/libcryptoa/src/lib.rs
|
||||||
|
use std::str::FromStr;
|
||||||
|
use age::{Decryptor, Encryptor, x25519};
|
||||||
|
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
|
||||||
|
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||||
|
use secrecy::ExposeSecret;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum AsymmetricCryptoError {
|
||||||
|
#[error("key parsing failed")]
|
||||||
|
ParseKey,
|
||||||
|
#[error("age crypto error: {0}")]
|
||||||
|
Age(String),
|
||||||
|
#[error("invalid utf-8 in plaintext")]
|
||||||
|
Utf8,
|
||||||
|
#[error("invalid signature length")]
|
||||||
|
SignatureLen,
|
||||||
|
#[error("signature verification failed")]
|
||||||
|
Verify,
|
||||||
|
#[error("base64 decoding failed: {0}")]
|
||||||
|
Base64(#[from] base64::DecodeError),
|
||||||
|
#[error("io error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_recipient(s: &str) -> Result<x25519::Recipient, AsymmetricCryptoError> {
|
||||||
|
x25519::Recipient::from_str(s).map_err(|_| AsymmetricCryptoError::ParseKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_identity(s: &str) -> Result<x25519::Identity, AsymmetricCryptoError> {
|
||||||
|
x25519::Identity::from_str(s).map_err(|_| AsymmetricCryptoError::ParseKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ed25519_signing_key(s: &str) -> Result<SigningKey, AsymmetricCryptoError> {
|
||||||
|
let bytes = B64.decode(s)?;
|
||||||
|
let key_bytes: [u8; 32] = bytes.try_into().map_err(|_| AsymmetricCryptoError::ParseKey)?;
|
||||||
|
Ok(SigningKey::from_bytes(&key_bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ed25519_verifying_key(s: &str) -> Result<VerifyingKey, AsymmetricCryptoError> {
|
||||||
|
let bytes = B64.decode(s)?;
|
||||||
|
let key_bytes: [u8; 32] = bytes.try_into().map_err(|_| AsymmetricCryptoError::ParseKey)?;
|
||||||
|
VerifyingKey::from_bytes(&key_bytes).map_err(|_| AsymmetricCryptoError::ParseKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gen_enc_keypair() -> (String, String) {
|
||||||
|
let id = x25519::Identity::generate();
|
||||||
|
let pk = id.to_public();
|
||||||
|
(pk.to_string(), id.to_string().expose_secret().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gen_sign_keypair() -> (String, String) {
|
||||||
|
let signing_key = SigningKey::generate(&mut rand::rngs::OsRng);
|
||||||
|
let verifying_key = signing_key.verifying_key();
|
||||||
|
(B64.encode(verifying_key.to_bytes()), B64.encode(signing_key.to_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt_b64(recipient_str: &str, msg: &str) -> Result<String, AsymmetricCryptoError> {
|
||||||
|
let recipient = parse_recipient(recipient_str)?;
|
||||||
|
let encryptor = Encryptor::with_recipients(vec![Box::new(recipient)])
|
||||||
|
.ok_or_else(|| AsymmetricCryptoError::Age("Failed to create encryptor".into()))?;
|
||||||
|
|
||||||
|
let mut encrypted = vec![];
|
||||||
|
let mut writer = encryptor.wrap_output(&mut encrypted)?;
|
||||||
|
std::io::Write::write_all(&mut writer, msg.as_bytes())?;
|
||||||
|
writer.finish()?;
|
||||||
|
|
||||||
|
Ok(B64.encode(encrypted))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt_b64(identity_str: &str, ct_b64: &str) -> Result<String, AsymmetricCryptoError> {
|
||||||
|
let identity = parse_identity(identity_str)?;
|
||||||
|
let ct = B64.decode(ct_b64)?;
|
||||||
|
|
||||||
|
let decryptor = Decryptor::new(&ct[..]).map_err(|e| AsymmetricCryptoError::Age(e.to_string()))?;
|
||||||
|
|
||||||
|
let mut decrypted = vec![];
|
||||||
|
if let Decryptor::Recipients(d) = decryptor {
|
||||||
|
let mut reader = d.decrypt(std::iter::once(&identity as &dyn age::Identity))
|
||||||
|
.map_err(|e| AsymmetricCryptoError::Age(e.to_string()))?;
|
||||||
|
std::io::Read::read_to_end(&mut reader, &mut decrypted)?;
|
||||||
|
String::from_utf8(decrypted).map_err(|_| AsymmetricCryptoError::Utf8)
|
||||||
|
} else {
|
||||||
|
Err(AsymmetricCryptoError::Age("Passphrase decryption not supported".into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sign_b64(signing_secret_str: &str, msg: &str) -> Result<String, AsymmetricCryptoError> {
|
||||||
|
let signing_key = parse_ed25519_signing_key(signing_secret_str)?;
|
||||||
|
let signature = signing_key.sign(msg.as_bytes());
|
||||||
|
Ok(B64.encode(signature.to_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_b64(verify_pub_str: &str, msg: &str, sig_b64: &str) -> Result<bool, AsymmetricCryptoError> {
|
||||||
|
let verifying_key = parse_ed25519_verifying_key(verify_pub_str)?;
|
||||||
|
let sig_bytes = B64.decode(sig_b64)?;
|
||||||
|
let signature = Signature::from_slice(&sig_bytes).map_err(|_| AsymmetricCryptoError::SignatureLen)?;
|
||||||
|
Ok(verifying_key.verify(msg.as_bytes(), &signature).is_ok())
|
||||||
|
}
|
15
crates/libdbstorage/Cargo.toml
Normal file
15
crates/libdbstorage/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "libdbstorage"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
redb = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
|
# Local Crate Dependencies
|
||||||
|
libcrypto = { path = "../libcrypto" }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
bincode = "1.3.3"
|
@@ -80,3 +80,10 @@ impl From<tokio::sync::mpsc::error::SendError<()>> for DBError {
|
|||||||
DBError(item.to_string().clone())
|
DBError(item.to_string().clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for DBError {
|
||||||
|
fn from(item: serde_json::Error) -> Self {
|
||||||
|
DBError(item.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
119
crates/libdbstorage/src/lib.rs
Normal file
119
crates/libdbstorage/src/lib.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
// In crates/libdbstorage/src/lib.rs
|
||||||
|
use std::{
|
||||||
|
path::Path,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
|
use libcrypto::CryptoFactory; // Correct import
|
||||||
|
use redb::{Database, TableDefinition};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub mod error; // Declare the error module
|
||||||
|
pub use error::DBError; // Re-export for users of this crate
|
||||||
|
|
||||||
|
// Declare storage module
|
||||||
|
pub mod storage;
|
||||||
|
|
||||||
|
// Table definitions for different Redis data types
|
||||||
|
const TYPES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("types");
|
||||||
|
const STRINGS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("strings");
|
||||||
|
const HASHES_TABLE: TableDefinition<(&str, &str), &[u8]> = TableDefinition::new("hashes");
|
||||||
|
const LISTS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("lists");
|
||||||
|
const STREAMS_META_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("streams_meta");
|
||||||
|
const STREAMS_DATA_TABLE: TableDefinition<(&str, &str), &[u8]> = TableDefinition::new("streams_data");
|
||||||
|
const ENCRYPTED_TABLE: TableDefinition<&str, u8> = TableDefinition::new("encrypted");
|
||||||
|
const EXPIRATION_TABLE: TableDefinition<&str, u64> = TableDefinition::new("expiration");
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct StreamEntry {
|
||||||
|
pub fields: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct ListValue {
|
||||||
|
pub elements: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn now_in_millis() -> u128 {
|
||||||
|
let start = SystemTime::now();
|
||||||
|
let duration_since_epoch = start.duration_since(UNIX_EPOCH).unwrap();
|
||||||
|
duration_since_epoch.as_millis()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Storage {
|
||||||
|
db: Database,
|
||||||
|
crypto: Option<CryptoFactory>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Storage {
|
||||||
|
pub fn new(path: impl AsRef<Path>, should_encrypt: bool, master_key: Option<&str>) -> Result<Self, DBError> {
|
||||||
|
let db = Database::create(path)?;
|
||||||
|
|
||||||
|
// Create tables if they don't exist
|
||||||
|
let write_txn = db.begin_write()?;
|
||||||
|
{
|
||||||
|
let _ = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
let _ = write_txn.open_table(STRINGS_TABLE)?;
|
||||||
|
let _ = write_txn.open_table(HASHES_TABLE)?;
|
||||||
|
let _ = write_txn.open_table(LISTS_TABLE)?;
|
||||||
|
let _ = write_txn.open_table(STREAMS_META_TABLE)?;
|
||||||
|
let _ = write_txn.open_table(STREAMS_DATA_TABLE)?;
|
||||||
|
let _ = write_txn.open_table(ENCRYPTED_TABLE)?;
|
||||||
|
let _ = write_txn.open_table(EXPIRATION_TABLE)?;
|
||||||
|
}
|
||||||
|
write_txn.commit()?;
|
||||||
|
|
||||||
|
// Check if database was previously encrypted
|
||||||
|
let read_txn = db.begin_read()?;
|
||||||
|
let encrypted_table = read_txn.open_table(ENCRYPTED_TABLE)?;
|
||||||
|
let was_encrypted = encrypted_table.get("encrypted")?.map(|v| v.value() == 1).unwrap_or(false);
|
||||||
|
drop(read_txn);
|
||||||
|
|
||||||
|
let crypto = if should_encrypt || was_encrypted {
|
||||||
|
if let Some(key) = master_key {
|
||||||
|
Some(CryptoFactory::new(key.as_bytes()))
|
||||||
|
} else {
|
||||||
|
return Err(DBError("Encryption requested but no master key provided".to_string()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// If we're enabling encryption for the first time, mark it
|
||||||
|
if should_encrypt && !was_encrypted {
|
||||||
|
let write_txn = db.begin_write()?;
|
||||||
|
{
|
||||||
|
let mut encrypted_table = write_txn.open_table(ENCRYPTED_TABLE)?;
|
||||||
|
encrypted_table.insert("encrypted", &1u8)?;
|
||||||
|
}
|
||||||
|
write_txn.commit()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Storage {
|
||||||
|
db,
|
||||||
|
crypto,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_encrypted(&self) -> bool {
|
||||||
|
self.crypto.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods for encryption
|
||||||
|
fn encrypt_if_needed(&self, data: &[u8]) -> Result<Vec<u8>, DBError> {
|
||||||
|
if let Some(crypto) = &self.crypto {
|
||||||
|
Ok(crypto.encrypt(data))
|
||||||
|
} else {
|
||||||
|
Ok(data.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_if_needed(&self, data: &[u8]) -> Result<Vec<u8>, DBError> {
|
||||||
|
if let Some(crypto) = &self.crypto {
|
||||||
|
Ok(crypto.decrypt(data).map_err(|e| DBError(e.to_string()))?)
|
||||||
|
} else {
|
||||||
|
Ok(data.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
crates/libdbstorage/src/storage/mod.rs
Normal file
4
crates/libdbstorage/src/storage/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod storage_basic;
|
||||||
|
pub mod storage_hset;
|
||||||
|
pub mod storage_lists;
|
||||||
|
pub mod storage_extra;
|
218
crates/libdbstorage/src/storage/storage_basic.rs
Normal file
218
crates/libdbstorage/src/storage/storage_basic.rs
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
use redb::{ReadableTable};
|
||||||
|
use crate::error::DBError;
|
||||||
|
use crate::{Storage, TYPES_TABLE, STRINGS_TABLE, HASHES_TABLE, LISTS_TABLE, STREAMS_META_TABLE, STREAMS_DATA_TABLE, EXPIRATION_TABLE, now_in_millis};
|
||||||
|
|
||||||
|
impl Storage {
|
||||||
|
pub fn flushdb(&self) -> Result<(), DBError> {
|
||||||
|
let write_txn = self.db.begin_write()?;
|
||||||
|
{
|
||||||
|
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
let mut strings_table = write_txn.open_table(STRINGS_TABLE)?;
|
||||||
|
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
|
||||||
|
let mut lists_table = write_txn.open_table(LISTS_TABLE)?;
|
||||||
|
let mut streams_meta_table = write_txn.open_table(STREAMS_META_TABLE)?;
|
||||||
|
let mut streams_data_table = write_txn.open_table(STREAMS_DATA_TABLE)?;
|
||||||
|
let mut expiration_table = write_txn.open_table(EXPIRATION_TABLE)?;
|
||||||
|
|
||||||
|
// inefficient, but there is no other way
|
||||||
|
let keys: Vec<String> = types_table.iter()?.map(|item| item.unwrap().0.value().to_string()).collect();
|
||||||
|
for key in keys {
|
||||||
|
types_table.remove(key.as_str())?;
|
||||||
|
}
|
||||||
|
let keys: Vec<String> = strings_table.iter()?.map(|item| item.unwrap().0.value().to_string()).collect();
|
||||||
|
for key in keys {
|
||||||
|
strings_table.remove(key.as_str())?;
|
||||||
|
}
|
||||||
|
let keys: Vec<(String, String)> = hashes_table
|
||||||
|
.iter()?
|
||||||
|
.map(|item| {
|
||||||
|
let binding = item.unwrap();
|
||||||
|
let (k, f) = binding.0.value();
|
||||||
|
(k.to_string(), f.to_string())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
for (key, field) in keys {
|
||||||
|
hashes_table.remove((key.as_str(), field.as_str()))?;
|
||||||
|
}
|
||||||
|
let keys: Vec<String> = lists_table.iter()?.map(|item| item.unwrap().0.value().to_string()).collect();
|
||||||
|
for key in keys {
|
||||||
|
lists_table.remove(key.as_str())?;
|
||||||
|
}
|
||||||
|
let keys: Vec<String> = streams_meta_table.iter()?.map(|item| item.unwrap().0.value().to_string()).collect();
|
||||||
|
for key in keys {
|
||||||
|
streams_meta_table.remove(key.as_str())?;
|
||||||
|
}
|
||||||
|
let keys: Vec<(String,String)> = streams_data_table.iter()?.map(|item| {
|
||||||
|
let binding = item.unwrap();
|
||||||
|
let (key, field) = binding.0.value();
|
||||||
|
(key.to_string(), field.to_string())
|
||||||
|
}).collect();
|
||||||
|
for (key, field) in keys {
|
||||||
|
streams_data_table.remove((key.as_str(), field.as_str()))?;
|
||||||
|
}
|
||||||
|
let keys: Vec<String> = expiration_table.iter()?.map(|item| item.unwrap().0.value().to_string()).collect();
|
||||||
|
for key in keys {
|
||||||
|
expiration_table.remove(key.as_str())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write_txn.commit()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_key_type(&self, key: &str) -> Result<Option<String>, DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
|
// Before returning type, check for expiration
|
||||||
|
if let Some(type_val) = table.get(key)? {
|
||||||
|
if type_val.value() == "string" {
|
||||||
|
let expiration_table = read_txn.open_table(EXPIRATION_TABLE)?;
|
||||||
|
if let Some(expires_at) = expiration_table.get(key)? {
|
||||||
|
if now_in_millis() > expires_at.value() as u128 {
|
||||||
|
// The key is expired, so it effectively has no type
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(type_val.value().to_string()))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENCRYPTION APPLIED: Value is encrypted/decrypted
|
||||||
|
pub fn get(&self, key: &str) -> Result<Option<String>, DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
|
||||||
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "string" => {
|
||||||
|
// Check expiration first (unencrypted)
|
||||||
|
let expiration_table = read_txn.open_table(EXPIRATION_TABLE)?;
|
||||||
|
if let Some(expires_at) = expiration_table.get(key)? {
|
||||||
|
if now_in_millis() > expires_at.value() as u128 {
|
||||||
|
drop(read_txn);
|
||||||
|
self.del(key.to_string())?;
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and decrypt value
|
||||||
|
let strings_table = read_txn.open_table(STRINGS_TABLE)?;
|
||||||
|
match strings_table.get(key)? {
|
||||||
|
Some(data) => {
|
||||||
|
let decrypted = self.decrypt_if_needed(data.value())?;
|
||||||
|
let value = String::from_utf8(decrypted)?;
|
||||||
|
Ok(Some(value))
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENCRYPTION APPLIED: Value is encrypted before storage
|
||||||
|
pub fn set(&self, key: String, value: String) -> Result<(), DBError> {
|
||||||
|
let write_txn = self.db.begin_write()?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
types_table.insert(key.as_str(), "string")?;
|
||||||
|
|
||||||
|
let mut strings_table = write_txn.open_table(STRINGS_TABLE)?;
|
||||||
|
// Only encrypt the value, not expiration
|
||||||
|
let encrypted = self.encrypt_if_needed(value.as_bytes())?;
|
||||||
|
strings_table.insert(key.as_str(), encrypted.as_slice())?;
|
||||||
|
|
||||||
|
// Remove any existing expiration since this is a regular SET
|
||||||
|
let mut expiration_table = write_txn.open_table(EXPIRATION_TABLE)?;
|
||||||
|
expiration_table.remove(key.as_str())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write_txn.commit()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENCRYPTION APPLIED: Value is encrypted before storage
|
||||||
|
pub fn setx(&self, key: String, value: String, expire_ms: u128) -> Result<(), DBError> {
|
||||||
|
let write_txn = self.db.begin_write()?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
types_table.insert(key.as_str(), "string")?;
|
||||||
|
|
||||||
|
let mut strings_table = write_txn.open_table(STRINGS_TABLE)?;
|
||||||
|
// Only encrypt the value
|
||||||
|
let encrypted = self.encrypt_if_needed(value.as_bytes())?;
|
||||||
|
strings_table.insert(key.as_str(), encrypted.as_slice())?;
|
||||||
|
|
||||||
|
// Store expiration separately (unencrypted)
|
||||||
|
let mut expiration_table = write_txn.open_table(EXPIRATION_TABLE)?;
|
||||||
|
let expires_at = expire_ms + now_in_millis();
|
||||||
|
expiration_table.insert(key.as_str(), &(expires_at as u64))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write_txn.commit()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn del(&self, key: String) -> Result<(), DBError> {
|
||||||
|
let write_txn = self.db.begin_write()?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
let mut strings_table = write_txn.open_table(STRINGS_TABLE)?;
|
||||||
|
let mut hashes_table: redb::Table<(&str, &str), &[u8]> = write_txn.open_table(HASHES_TABLE)?;
|
||||||
|
let mut lists_table = write_txn.open_table(LISTS_TABLE)?;
|
||||||
|
|
||||||
|
// Remove from type table
|
||||||
|
types_table.remove(key.as_str())?;
|
||||||
|
|
||||||
|
// Remove from strings table
|
||||||
|
strings_table.remove(key.as_str())?;
|
||||||
|
|
||||||
|
// Remove all hash fields for this key
|
||||||
|
let mut to_remove = Vec::new();
|
||||||
|
let mut iter = hashes_table.iter()?;
|
||||||
|
while let Some(entry) = iter.next() {
|
||||||
|
let entry = entry?;
|
||||||
|
let (hash_key, field) = entry.0.value();
|
||||||
|
if hash_key == key.as_str() {
|
||||||
|
to_remove.push((hash_key.to_string(), field.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(iter);
|
||||||
|
|
||||||
|
for (hash_key, field) in to_remove {
|
||||||
|
hashes_table.remove((hash_key.as_str(), field.as_str()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from lists table
|
||||||
|
lists_table.remove(key.as_str())?;
|
||||||
|
|
||||||
|
// Also remove expiration
|
||||||
|
let mut expiration_table = write_txn.open_table(EXPIRATION_TABLE)?;
|
||||||
|
expiration_table.remove(key.as_str())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write_txn.commit()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keys(&self, pattern: &str) -> Result<Vec<String>, DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
|
let mut keys = Vec::new();
|
||||||
|
let mut iter = table.iter()?;
|
||||||
|
while let Some(entry) = iter.next() {
|
||||||
|
let key = entry?.0.value().to_string();
|
||||||
|
if pattern == "*" || super::storage_extra::glob_match(pattern, &key) {
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(keys)
|
||||||
|
}
|
||||||
|
}
|
168
crates/libdbstorage/src/storage/storage_extra.rs
Normal file
168
crates/libdbstorage/src/storage/storage_extra.rs
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
use redb::{ReadableTable};
|
||||||
|
use crate::error::DBError;
|
||||||
|
use crate::{Storage, TYPES_TABLE, STRINGS_TABLE, EXPIRATION_TABLE, now_in_millis};
|
||||||
|
|
||||||
|
impl Storage {
|
||||||
|
// ✅ ENCRYPTION APPLIED: Values are decrypted after retrieval
|
||||||
|
pub fn scan(&self, cursor: u64, pattern: Option<&str>, count: Option<u64>) -> Result<(u64, Vec<(String, String)>), DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
let strings_table = read_txn.open_table(STRINGS_TABLE)?;
|
||||||
|
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let mut current_cursor = 0u64;
|
||||||
|
let limit = count.unwrap_or(10) as usize;
|
||||||
|
|
||||||
|
let mut iter = types_table.iter()?;
|
||||||
|
while let Some(entry) = iter.next() {
|
||||||
|
let entry = entry?;
|
||||||
|
let key = entry.0.value().to_string();
|
||||||
|
let key_type = entry.1.value().to_string();
|
||||||
|
|
||||||
|
if current_cursor >= cursor {
|
||||||
|
// Apply pattern matching if specified
|
||||||
|
let matches = if let Some(pat) = pattern {
|
||||||
|
glob_match(pat, &key)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
};
|
||||||
|
|
||||||
|
if matches {
|
||||||
|
// For scan, we return key-value pairs for string types
|
||||||
|
if key_type == "string" {
|
||||||
|
if let Some(data) = strings_table.get(key.as_str())? {
|
||||||
|
let decrypted = self.decrypt_if_needed(data.value())?;
|
||||||
|
let value = String::from_utf8(decrypted)?;
|
||||||
|
result.push((key, value));
|
||||||
|
} else {
|
||||||
|
result.push((key, String::new()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For non-string types, just return the key with type as value
|
||||||
|
result.push((key, key_type));
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.len() >= limit {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current_cursor += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let next_cursor = if result.len() < limit { 0 } else { current_cursor };
|
||||||
|
Ok((next_cursor, result))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ttl(&self, key: &str) -> Result<i64, DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
|
match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "string" => {
|
||||||
|
let expiration_table = read_txn.open_table(EXPIRATION_TABLE)?;
|
||||||
|
match expiration_table.get(key)? {
|
||||||
|
Some(expires_at) => {
|
||||||
|
let now = now_in_millis();
|
||||||
|
let expires_at_ms = expires_at.value() as u128;
|
||||||
|
if now >= expires_at_ms {
|
||||||
|
Ok(-2) // Key has expired
|
||||||
|
} else {
|
||||||
|
Ok(((expires_at_ms - now) / 1000) as i64) // TTL in seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Ok(-1), // Key exists but has no expiration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) => Ok(-1), // Key exists but is not a string (no expiration support for other types)
|
||||||
|
None => Ok(-2), // Key does not exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exists(&self, key: &str) -> Result<bool, DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
|
match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "string" => {
|
||||||
|
// Check if string key has expired
|
||||||
|
let expiration_table = read_txn.open_table(EXPIRATION_TABLE)?;
|
||||||
|
if let Some(expires_at) = expiration_table.get(key)? {
|
||||||
|
if now_in_millis() > expires_at.value() as u128 {
|
||||||
|
return Ok(false); // Key has expired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
Some(_) => Ok(true), // Key exists and is not a string
|
||||||
|
None => Ok(false), // Key does not exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility function for glob pattern matching
|
||||||
|
pub fn glob_match(pattern: &str, text: &str) -> bool {
|
||||||
|
if pattern == "*" {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple glob matching - supports * and ? wildcards
|
||||||
|
let pattern_chars: Vec<char> = pattern.chars().collect();
|
||||||
|
let text_chars: Vec<char> = text.chars().collect();
|
||||||
|
|
||||||
|
fn match_recursive(pattern: &[char], text: &[char], pi: usize, ti: usize) -> bool {
|
||||||
|
if pi >= pattern.len() {
|
||||||
|
return ti >= text.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ti >= text.len() {
|
||||||
|
// Check if remaining pattern is all '*'
|
||||||
|
return pattern[pi..].iter().all(|&c| c == '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
match pattern[pi] {
|
||||||
|
'*' => {
|
||||||
|
// Try matching zero or more characters
|
||||||
|
for i in ti..=text.len() {
|
||||||
|
if match_recursive(pattern, text, pi + 1, i) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
'?' => {
|
||||||
|
// Match exactly one character
|
||||||
|
match_recursive(pattern, text, pi + 1, ti + 1)
|
||||||
|
}
|
||||||
|
c => {
|
||||||
|
// Match exact character
|
||||||
|
if text[ti] == c {
|
||||||
|
match_recursive(pattern, text, pi + 1, ti + 1)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match_recursive(&pattern_chars, &text_chars, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_glob_match() {
|
||||||
|
assert!(glob_match("*", "anything"));
|
||||||
|
assert!(glob_match("hello", "hello"));
|
||||||
|
assert!(!glob_match("hello", "world"));
|
||||||
|
assert!(glob_match("h*o", "hello"));
|
||||||
|
assert!(glob_match("h*o", "ho"));
|
||||||
|
assert!(!glob_match("h*o", "hi"));
|
||||||
|
assert!(glob_match("h?llo", "hello"));
|
||||||
|
assert!(!glob_match("h?llo", "hllo"));
|
||||||
|
assert!(glob_match("*test*", "this_is_a_test_string"));
|
||||||
|
assert!(!glob_match("*test*", "this_is_a_string"));
|
||||||
|
}
|
||||||
|
}
|
318
crates/libdbstorage/src/storage/storage_hset.rs
Normal file
318
crates/libdbstorage/src/storage/storage_hset.rs
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
use redb::{ReadableTable};
|
||||||
|
use crate::error::DBError;
|
||||||
|
use crate::{Storage, TYPES_TABLE, HASHES_TABLE};
|
||||||
|
|
||||||
|
impl Storage {
|
||||||
|
// ✅ ENCRYPTION APPLIED: Values are encrypted before storage
|
||||||
|
pub fn hset(&self, key: &str, pairs: Vec<(String, String)>) -> Result<i64, DBError> {
|
||||||
|
let write_txn = self.db.begin_write()?;
|
||||||
|
let mut new_fields = 0i64;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
|
||||||
|
|
||||||
|
// Set the type to hash
|
||||||
|
types_table.insert(key, "hash")?;
|
||||||
|
|
||||||
|
for (field, value) in pairs {
|
||||||
|
// Check if field already exists
|
||||||
|
let exists = hashes_table.get((key, field.as_str()))?.is_some();
|
||||||
|
|
||||||
|
// Encrypt the value before storing
|
||||||
|
let encrypted = self.encrypt_if_needed(value.as_bytes())?;
|
||||||
|
hashes_table.insert((key, field.as_str()), encrypted.as_slice())?;
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
new_fields += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_txn.commit()?;
|
||||||
|
Ok(new_fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENCRYPTION APPLIED: Value is decrypted after retrieval
|
||||||
|
pub fn hget(&self, key: &str, field: &str) -> Result<Option<String>, DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
|
match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "hash" => {
|
||||||
|
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||||
|
match hashes_table.get((key, field))? {
|
||||||
|
Some(data) => {
|
||||||
|
let decrypted = self.decrypt_if_needed(data.value())?;
|
||||||
|
let value = String::from_utf8(decrypted)?;
|
||||||
|
Ok(Some(value))
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENCRYPTION APPLIED: All values are decrypted after retrieval
|
||||||
|
pub fn hgetall(&self, key: &str) -> Result<Vec<(String, String)>, DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
|
match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "hash" => {
|
||||||
|
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
let mut iter = hashes_table.iter()?;
|
||||||
|
while let Some(entry) = iter.next() {
|
||||||
|
let entry = entry?;
|
||||||
|
let (hash_key, field) = entry.0.value();
|
||||||
|
if hash_key == key {
|
||||||
|
let decrypted = self.decrypt_if_needed(entry.1.value())?;
|
||||||
|
let value = String::from_utf8(decrypted)?;
|
||||||
|
result.push((field.to_string(), value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
_ => Ok(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hdel(&self, key: &str, fields: Vec<String>) -> Result<i64, DBError> {
|
||||||
|
let write_txn = self.db.begin_write()?;
|
||||||
|
let mut deleted = 0i64;
|
||||||
|
|
||||||
|
// First check if key exists and is a hash
|
||||||
|
let is_hash = {
|
||||||
|
let types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
let result = match types_table.get(key)? {
|
||||||
|
Some(type_val) => type_val.value() == "hash",
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
result
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_hash {
|
||||||
|
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
|
||||||
|
|
||||||
|
for field in fields {
|
||||||
|
if hashes_table.remove((key, field.as_str()))?.is_some() {
|
||||||
|
deleted += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if hash is now empty and remove type if so
|
||||||
|
let mut has_fields = false;
|
||||||
|
let mut iter = hashes_table.iter()?;
|
||||||
|
while let Some(entry) = iter.next() {
|
||||||
|
let entry = entry?;
|
||||||
|
let (hash_key, _) = entry.0.value();
|
||||||
|
if hash_key == key {
|
||||||
|
has_fields = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(iter);
|
||||||
|
|
||||||
|
if !has_fields {
|
||||||
|
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
types_table.remove(key)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_txn.commit()?;
|
||||||
|
Ok(deleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hexists(&self, key: &str, field: &str) -> Result<bool, DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
|
match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "hash" => {
|
||||||
|
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||||
|
Ok(hashes_table.get((key, field))?.is_some())
|
||||||
|
}
|
||||||
|
_ => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hkeys(&self, key: &str) -> Result<Vec<String>, DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
|
match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "hash" => {
|
||||||
|
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
let mut iter = hashes_table.iter()?;
|
||||||
|
while let Some(entry) = iter.next() {
|
||||||
|
let entry = entry?;
|
||||||
|
let (hash_key, field) = entry.0.value();
|
||||||
|
if hash_key == key {
|
||||||
|
result.push(field.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
_ => Ok(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENCRYPTION APPLIED: All values are decrypted after retrieval
|
||||||
|
pub fn hvals(&self, key: &str) -> Result<Vec<String>, DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
|
match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "hash" => {
|
||||||
|
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
let mut iter = hashes_table.iter()?;
|
||||||
|
while let Some(entry) = iter.next() {
|
||||||
|
let entry = entry?;
|
||||||
|
let (hash_key, _) = entry.0.value();
|
||||||
|
if hash_key == key {
|
||||||
|
let decrypted = self.decrypt_if_needed(entry.1.value())?;
|
||||||
|
let value = String::from_utf8(decrypted)?;
|
||||||
|
result.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
_ => Ok(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hlen(&self, key: &str) -> Result<i64, DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
|
match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "hash" => {
|
||||||
|
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||||
|
let mut count = 0i64;
|
||||||
|
|
||||||
|
let mut iter = hashes_table.iter()?;
|
||||||
|
while let Some(entry) = iter.next() {
|
||||||
|
let entry = entry?;
|
||||||
|
let (hash_key, _) = entry.0.value();
|
||||||
|
if hash_key == key {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
_ => Ok(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENCRYPTION APPLIED: Values are decrypted after retrieval
|
||||||
|
pub fn hmget(&self, key: &str, fields: Vec<String>) -> Result<Vec<Option<String>>, DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
|
match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "hash" => {
|
||||||
|
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
for field in fields {
|
||||||
|
match hashes_table.get((key, field.as_str()))? {
|
||||||
|
Some(data) => {
|
||||||
|
let decrypted = self.decrypt_if_needed(data.value())?;
|
||||||
|
let value = String::from_utf8(decrypted)?;
|
||||||
|
result.push(Some(value));
|
||||||
|
}
|
||||||
|
None => result.push(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
_ => Ok(fields.into_iter().map(|_| None).collect()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENCRYPTION APPLIED: Value is encrypted before storage
|
||||||
|
pub fn hsetnx(&self, key: &str, field: &str, value: &str) -> Result<bool, DBError> {
|
||||||
|
let write_txn = self.db.begin_write()?;
|
||||||
|
let mut result = false;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
|
||||||
|
|
||||||
|
// Check if field already exists
|
||||||
|
if hashes_table.get((key, field))?.is_none() {
|
||||||
|
// Set the type to hash
|
||||||
|
types_table.insert(key, "hash")?;
|
||||||
|
|
||||||
|
// Encrypt the value before storing
|
||||||
|
let encrypted = self.encrypt_if_needed(value.as_bytes())?;
|
||||||
|
hashes_table.insert((key, field), encrypted.as_slice())?;
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_txn.commit()?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENCRYPTION APPLIED: Values are decrypted after retrieval
|
||||||
|
pub fn hscan(&self, key: &str, cursor: u64, pattern: Option<&str>, count: Option<u64>) -> Result<(u64, Vec<(String, String)>), DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
|
match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "hash" => {
|
||||||
|
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let mut current_cursor = 0u64;
|
||||||
|
let limit = count.unwrap_or(10) as usize;
|
||||||
|
|
||||||
|
let mut iter = hashes_table.iter()?;
|
||||||
|
while let Some(entry) = iter.next() {
|
||||||
|
let entry = entry?;
|
||||||
|
let (hash_key, field) = entry.0.value();
|
||||||
|
|
||||||
|
if hash_key == key {
|
||||||
|
if current_cursor >= cursor {
|
||||||
|
let field_str = field.to_string();
|
||||||
|
|
||||||
|
// Apply pattern matching if specified
|
||||||
|
let matches = if let Some(pat) = pattern {
|
||||||
|
super::storage_extra::glob_match(pat, &field_str)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
};
|
||||||
|
|
||||||
|
if matches {
|
||||||
|
let decrypted = self.decrypt_if_needed(entry.1.value())?;
|
||||||
|
let value = String::from_utf8(decrypted)?;
|
||||||
|
result.push((field_str, value));
|
||||||
|
|
||||||
|
if result.len() >= limit {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current_cursor += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let next_cursor = if result.len() < limit { 0 } else { current_cursor };
|
||||||
|
Ok((next_cursor, result))
|
||||||
|
}
|
||||||
|
_ => Ok((0, Vec::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
403
crates/libdbstorage/src/storage/storage_lists.rs
Normal file
403
crates/libdbstorage/src/storage/storage_lists.rs
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
use redb::{ReadableTable};
|
||||||
|
use crate::error::DBError;
|
||||||
|
use crate::{Storage, TYPES_TABLE, LISTS_TABLE};
|
||||||
|
|
||||||
|
impl Storage {
|
||||||
|
// ✅ ENCRYPTION APPLIED: Elements are encrypted before storage
|
||||||
|
pub fn lpush(&self, key: &str, elements: Vec<String>) -> Result<i64, DBError> {
|
||||||
|
let write_txn = self.db.begin_write()?;
|
||||||
|
let mut _length = 0i64;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
let mut lists_table = write_txn.open_table(LISTS_TABLE)?;
|
||||||
|
|
||||||
|
// Set the type to list
|
||||||
|
types_table.insert(key, "list")?;
|
||||||
|
|
||||||
|
// Get current list or create empty one
|
||||||
|
let mut list: Vec<String> = match lists_table.get(key)? {
|
||||||
|
Some(data) => {
|
||||||
|
let decrypted = self.decrypt_if_needed(data.value())?;
|
||||||
|
serde_json::from_slice(&decrypted)?
|
||||||
|
}
|
||||||
|
None => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add elements to the front (left)
|
||||||
|
for element in elements.into_iter().rev() {
|
||||||
|
list.insert(0, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
_length = list.len() as i64;
|
||||||
|
|
||||||
|
// Encrypt and store the updated list
|
||||||
|
let serialized = serde_json::to_vec(&list)?;
|
||||||
|
let encrypted = self.encrypt_if_needed(&serialized)?;
|
||||||
|
lists_table.insert(key, encrypted.as_slice())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write_txn.commit()?;
|
||||||
|
Ok(_length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENCRYPTION APPLIED: Elements are encrypted before storage
|
||||||
|
pub fn rpush(&self, key: &str, elements: Vec<String>) -> Result<i64, DBError> {
|
||||||
|
let write_txn = self.db.begin_write()?;
|
||||||
|
let mut _length = 0i64;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
let mut lists_table = write_txn.open_table(LISTS_TABLE)?;
|
||||||
|
|
||||||
|
// Set the type to list
|
||||||
|
types_table.insert(key, "list")?;
|
||||||
|
|
||||||
|
// Get current list or create empty one
|
||||||
|
let mut list: Vec<String> = match lists_table.get(key)? {
|
||||||
|
Some(data) => {
|
||||||
|
let decrypted = self.decrypt_if_needed(data.value())?;
|
||||||
|
serde_json::from_slice(&decrypted)?
|
||||||
|
}
|
||||||
|
None => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add elements to the end (right)
|
||||||
|
list.extend(elements);
|
||||||
|
_length = list.len() as i64;
|
||||||
|
|
||||||
|
// Encrypt and store the updated list
|
||||||
|
let serialized = serde_json::to_vec(&list)?;
|
||||||
|
let encrypted = self.encrypt_if_needed(&serialized)?;
|
||||||
|
lists_table.insert(key, encrypted.as_slice())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write_txn.commit()?;
|
||||||
|
Ok(_length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENCRYPTION APPLIED: Elements are decrypted after retrieval and encrypted before storage
|
||||||
|
pub fn lpop(&self, key: &str, count: u64) -> Result<Vec<String>, DBError> {
|
||||||
|
let write_txn = self.db.begin_write()?;
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
// First check if key exists and is a list, and get the data
|
||||||
|
let list_data = {
|
||||||
|
let types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
let lists_table = write_txn.open_table(LISTS_TABLE)?;
|
||||||
|
|
||||||
|
let result = match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "list" => {
|
||||||
|
if let Some(data) = lists_table.get(key)? {
|
||||||
|
let decrypted = self.decrypt_if_needed(data.value())?;
|
||||||
|
let list: Vec<String> = serde_json::from_slice(&decrypted)?;
|
||||||
|
Some(list)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
result
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(mut list) = list_data {
|
||||||
|
let pop_count = std::cmp::min(count as usize, list.len());
|
||||||
|
for _ in 0..pop_count {
|
||||||
|
if !list.is_empty() {
|
||||||
|
result.push(list.remove(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut lists_table = write_txn.open_table(LISTS_TABLE)?;
|
||||||
|
if list.is_empty() {
|
||||||
|
// Remove the key if list is empty
|
||||||
|
lists_table.remove(key)?;
|
||||||
|
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
types_table.remove(key)?;
|
||||||
|
} else {
|
||||||
|
// Encrypt and store the updated list
|
||||||
|
let serialized = serde_json::to_vec(&list)?;
|
||||||
|
let encrypted = self.encrypt_if_needed(&serialized)?;
|
||||||
|
lists_table.insert(key, encrypted.as_slice())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_txn.commit()?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENCRYPTION APPLIED: Elements are decrypted after retrieval and encrypted before storage
|
||||||
|
pub fn rpop(&self, key: &str, count: u64) -> Result<Vec<String>, DBError> {
|
||||||
|
let write_txn = self.db.begin_write()?;
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
// First check if key exists and is a list, and get the data
|
||||||
|
let list_data = {
|
||||||
|
let types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
let lists_table = write_txn.open_table(LISTS_TABLE)?;
|
||||||
|
|
||||||
|
let result = match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "list" => {
|
||||||
|
if let Some(data) = lists_table.get(key)? {
|
||||||
|
let decrypted = self.decrypt_if_needed(data.value())?;
|
||||||
|
let list: Vec<String> = serde_json::from_slice(&decrypted)?;
|
||||||
|
Some(list)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
result
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(mut list) = list_data {
|
||||||
|
let pop_count = std::cmp::min(count as usize, list.len());
|
||||||
|
for _ in 0..pop_count {
|
||||||
|
if !list.is_empty() {
|
||||||
|
result.push(list.pop().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut lists_table = write_txn.open_table(LISTS_TABLE)?;
|
||||||
|
if list.is_empty() {
|
||||||
|
// Remove the key if list is empty
|
||||||
|
lists_table.remove(key)?;
|
||||||
|
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
types_table.remove(key)?;
|
||||||
|
} else {
|
||||||
|
// Encrypt and store the updated list
|
||||||
|
let serialized = serde_json::to_vec(&list)?;
|
||||||
|
let encrypted = self.encrypt_if_needed(&serialized)?;
|
||||||
|
lists_table.insert(key, encrypted.as_slice())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_txn.commit()?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn llen(&self, key: &str) -> Result<i64, DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
|
match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "list" => {
|
||||||
|
let lists_table = read_txn.open_table(LISTS_TABLE)?;
|
||||||
|
match lists_table.get(key)? {
|
||||||
|
Some(data) => {
|
||||||
|
let decrypted = self.decrypt_if_needed(data.value())?;
|
||||||
|
let list: Vec<String> = serde_json::from_slice(&decrypted)?;
|
||||||
|
Ok(list.len() as i64)
|
||||||
|
}
|
||||||
|
None => Ok(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Ok(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENCRYPTION APPLIED: Element is decrypted after retrieval
|
||||||
|
pub fn lindex(&self, key: &str, index: i64) -> Result<Option<String>, DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
|
match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "list" => {
|
||||||
|
let lists_table = read_txn.open_table(LISTS_TABLE)?;
|
||||||
|
match lists_table.get(key)? {
|
||||||
|
Some(data) => {
|
||||||
|
let decrypted = self.decrypt_if_needed(data.value())?;
|
||||||
|
let list: Vec<String> = serde_json::from_slice(&decrypted)?;
|
||||||
|
|
||||||
|
let actual_index = if index < 0 {
|
||||||
|
list.len() as i64 + index
|
||||||
|
} else {
|
||||||
|
index
|
||||||
|
};
|
||||||
|
|
||||||
|
if actual_index >= 0 && (actual_index as usize) < list.len() {
|
||||||
|
Ok(Some(list[actual_index as usize].clone()))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENCRYPTION APPLIED: Elements are decrypted after retrieval
|
||||||
|
pub fn lrange(&self, key: &str, start: i64, stop: i64) -> Result<Vec<String>, DBError> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
|
match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "list" => {
|
||||||
|
let lists_table = read_txn.open_table(LISTS_TABLE)?;
|
||||||
|
match lists_table.get(key)? {
|
||||||
|
Some(data) => {
|
||||||
|
let decrypted = self.decrypt_if_needed(data.value())?;
|
||||||
|
let list: Vec<String> = serde_json::from_slice(&decrypted)?;
|
||||||
|
|
||||||
|
if list.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = list.len() as i64;
|
||||||
|
let start_idx = if start < 0 { std::cmp::max(0, len + start) } else { std::cmp::min(start, len) };
|
||||||
|
let stop_idx = if stop < 0 { std::cmp::max(-1, len + stop) } else { std::cmp::min(stop, len - 1) };
|
||||||
|
|
||||||
|
if start_idx > stop_idx || start_idx >= len {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let start_usize = start_idx as usize;
|
||||||
|
let stop_usize = (stop_idx + 1) as usize;
|
||||||
|
|
||||||
|
Ok(list[start_usize..std::cmp::min(stop_usize, list.len())].to_vec())
|
||||||
|
}
|
||||||
|
None => Ok(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Ok(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENCRYPTION APPLIED: Elements are decrypted after retrieval and encrypted before storage
|
||||||
|
pub fn ltrim(&self, key: &str, start: i64, stop: i64) -> Result<(), DBError> {
|
||||||
|
let write_txn = self.db.begin_write()?;
|
||||||
|
|
||||||
|
// First check if key exists and is a list, and get the data
|
||||||
|
let list_data = {
|
||||||
|
let types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
let lists_table = write_txn.open_table(LISTS_TABLE)?;
|
||||||
|
|
||||||
|
let result = match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "list" => {
|
||||||
|
if let Some(data) = lists_table.get(key)? {
|
||||||
|
let decrypted = self.decrypt_if_needed(data.value())?;
|
||||||
|
let list: Vec<String> = serde_json::from_slice(&decrypted)?;
|
||||||
|
Some(list)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
result
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(list) = list_data {
|
||||||
|
if list.is_empty() {
|
||||||
|
write_txn.commit()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = list.len() as i64;
|
||||||
|
let start_idx = if start < 0 { std::cmp::max(0, len + start) } else { std::cmp::min(start, len) };
|
||||||
|
let stop_idx = if stop < 0 { std::cmp::max(-1, len + stop) } else { std::cmp::min(stop, len - 1) };
|
||||||
|
|
||||||
|
let mut lists_table = write_txn.open_table(LISTS_TABLE)?;
|
||||||
|
if start_idx > stop_idx || start_idx >= len {
|
||||||
|
// Remove the entire list
|
||||||
|
lists_table.remove(key)?;
|
||||||
|
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
types_table.remove(key)?;
|
||||||
|
} else {
|
||||||
|
let start_usize = start_idx as usize;
|
||||||
|
let stop_usize = (stop_idx + 1) as usize;
|
||||||
|
let trimmed = list[start_usize..std::cmp::min(stop_usize, list.len())].to_vec();
|
||||||
|
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
lists_table.remove(key)?;
|
||||||
|
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
types_table.remove(key)?;
|
||||||
|
} else {
|
||||||
|
// Encrypt and store the trimmed list
|
||||||
|
let serialized = serde_json::to_vec(&trimmed)?;
|
||||||
|
let encrypted = self.encrypt_if_needed(&serialized)?;
|
||||||
|
lists_table.insert(key, encrypted.as_slice())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_txn.commit()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENCRYPTION APPLIED: Elements are decrypted after retrieval and encrypted before storage
|
||||||
|
pub fn lrem(&self, key: &str, count: i64, element: &str) -> Result<i64, DBError> {
|
||||||
|
let write_txn = self.db.begin_write()?;
|
||||||
|
let mut removed = 0i64;
|
||||||
|
|
||||||
|
// First check if key exists and is a list, and get the data
|
||||||
|
let list_data = {
|
||||||
|
let types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
let lists_table = write_txn.open_table(LISTS_TABLE)?;
|
||||||
|
|
||||||
|
let result = match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "list" => {
|
||||||
|
if let Some(data) = lists_table.get(key)? {
|
||||||
|
let decrypted = self.decrypt_if_needed(data.value())?;
|
||||||
|
let list: Vec<String> = serde_json::from_slice(&decrypted)?;
|
||||||
|
Some(list)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
result
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(mut list) = list_data {
|
||||||
|
if count == 0 {
|
||||||
|
// Remove all occurrences
|
||||||
|
let original_len = list.len();
|
||||||
|
list.retain(|x| x != element);
|
||||||
|
removed = (original_len - list.len()) as i64;
|
||||||
|
} else if count > 0 {
|
||||||
|
// Remove first count occurrences
|
||||||
|
let mut to_remove = count as usize;
|
||||||
|
list.retain(|x| {
|
||||||
|
if x == element && to_remove > 0 {
|
||||||
|
to_remove -= 1;
|
||||||
|
removed += 1;
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Remove last |count| occurrences
|
||||||
|
let mut to_remove = (-count) as usize;
|
||||||
|
for i in (0..list.len()).rev() {
|
||||||
|
if list[i] == element && to_remove > 0 {
|
||||||
|
list.remove(i);
|
||||||
|
to_remove -= 1;
|
||||||
|
removed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut lists_table = write_txn.open_table(LISTS_TABLE)?;
|
||||||
|
if list.is_empty() {
|
||||||
|
lists_table.remove(key)?;
|
||||||
|
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
|
types_table.remove(key)?;
|
||||||
|
} else {
|
||||||
|
// Encrypt and store the updated list
|
||||||
|
let serialized = serde_json::to_vec(&list)?;
|
||||||
|
let encrypted = self.encrypt_if_needed(&serialized)?;
|
||||||
|
lists_table.insert(key, encrypted.as_slice())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_txn.commit()?;
|
||||||
|
Ok(removed)
|
||||||
|
}
|
||||||
|
}
|
9
crates/supervisor/Cargo.toml
Normal file
9
crates/supervisor/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "supervisor"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# The supervisor will eventually depend on the herodb crate.
|
||||||
|
# We can add this dependency now.
|
||||||
|
# herodb = { path = "../herodb" }
|
4
crates/supervisor/src/main.rs
Normal file
4
crates/supervisor/src/main.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("Hello from the supervisor crate!");
|
||||||
|
// Supervisor logic will be implemented here.
|
||||||
|
}
|
18
crates/supervisorrpc/Cargo.toml
Normal file
18
crates/supervisorrpc/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "supervisorrpc"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "supervisorrpc"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Example dependencies for an RPC server
|
||||||
|
# axum = "0.7"
|
||||||
|
# jsonrpsee = { version = "0.22", features = ["server"] }
|
||||||
|
# openrpc-types = "0.7"
|
||||||
|
|
||||||
|
tokio = { workspace = true }
|
||||||
|
redis = { version = "0.24", features = ["tokio-comp"] }
|
||||||
|
herocrypto = { path = "../herocrypto" }
|
12
crates/supervisorrpc/src/main.rs
Normal file
12
crates/supervisorrpc/src/main.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// To be implemented:
|
||||||
|
// 1. Define an OpenRPC schema for supervisor functions (e.g., server status, key rotation).
|
||||||
|
// 2. Implement an HTTP/TCP server (e.g., using Axum or jsonrpsee) that serves the schema
|
||||||
|
// and handles RPC calls.
|
||||||
|
// 3. Implement support for Unix domain sockets in addition to TCP.
|
||||||
|
// 4. Use the `herocrypto` or `redis-rs` crate to interact with the main `herodb` instance.
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
println!("Supervisor RPC server starting... (not implemented)");
|
||||||
|
// Server setup code will go here.
|
||||||
|
}
|
@@ -1,100 +0,0 @@
|
|||||||
Perfect — here’s a tiny “factory” you can drop in.
|
|
||||||
|
|
||||||
### Cargo.toml
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
chacha20poly1305 = { version = "0.10", features = ["xchacha20"] }
|
|
||||||
rand = "0.8"
|
|
||||||
sha2 = "0.10"
|
|
||||||
```
|
|
||||||
|
|
||||||
### `crypto_factory.rs`
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use chacha20poly1305::{
|
|
||||||
aead::{Aead, KeyInit, OsRng},
|
|
||||||
XChaCha20Poly1305, Key, XNonce,
|
|
||||||
};
|
|
||||||
use rand::RngCore;
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
|
|
||||||
const VERSION: u8 = 1;
|
|
||||||
const NONCE_LEN: usize = 24;
|
|
||||||
const TAG_LEN: usize = 16;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum CryptoError {
|
|
||||||
Format, // wrong length / header
|
|
||||||
Version(u8), // unknown version
|
|
||||||
Decrypt, // wrong key or corrupted data
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Super-simple factory: new(secret) + encrypt(bytes) + decrypt(bytes)
|
|
||||||
pub struct CryptoFactory {
|
|
||||||
key: Key<XChaCha20Poly1305>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CryptoFactory {
|
|
||||||
/// Accepts any secret bytes; turns them into a 32-byte key (SHA-256).
|
|
||||||
/// (If your secret is already 32 bytes, this is still fine.)
|
|
||||||
pub fn new<S: AsRef<[u8]>>(secret: S) -> Self {
|
|
||||||
let mut h = Sha256::new();
|
|
||||||
h.update(b"xchacha20poly1305-factory:v1"); // domain separation
|
|
||||||
h.update(secret.as_ref());
|
|
||||||
let digest = h.finalize(); // 32 bytes
|
|
||||||
let key = Key::<XChaCha20Poly1305>::from_slice(&digest).to_owned();
|
|
||||||
Self { key }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Output layout: [version:1][nonce:24][ciphertext||tag]
|
|
||||||
pub fn encrypt(&self, plaintext: &[u8]) -> Vec<u8> {
|
|
||||||
let cipher = XChaCha20Poly1305::new(&self.key);
|
|
||||||
|
|
||||||
let mut nonce_bytes = [0u8; NONCE_LEN];
|
|
||||||
OsRng.fill_bytes(&mut nonce_bytes);
|
|
||||||
let nonce = XNonce::from_slice(&nonce_bytes);
|
|
||||||
|
|
||||||
let mut out = Vec::with_capacity(1 + NONCE_LEN + plaintext.len() + TAG_LEN);
|
|
||||||
out.push(VERSION);
|
|
||||||
out.extend_from_slice(&nonce_bytes);
|
|
||||||
|
|
||||||
let ct = cipher.encrypt(nonce, plaintext).expect("encrypt");
|
|
||||||
out.extend_from_slice(&ct);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decrypt(&self, blob: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
|
||||||
if blob.len() < 1 + NONCE_LEN + TAG_LEN {
|
|
||||||
return Err(CryptoError::Format);
|
|
||||||
}
|
|
||||||
let ver = blob[0];
|
|
||||||
if ver != VERSION {
|
|
||||||
return Err(CryptoError::Version(ver));
|
|
||||||
}
|
|
||||||
|
|
||||||
let nonce = XNonce::from_slice(&blob[1..1 + NONCE_LEN]);
|
|
||||||
let ct = &blob[1 + NONCE_LEN..];
|
|
||||||
|
|
||||||
let cipher = XChaCha20Poly1305::new(&self.key);
|
|
||||||
cipher.decrypt(nonce, ct).map_err(|_| CryptoError::Decrypt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tiny usage example
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn main() {
|
|
||||||
let f = CryptoFactory::new(b"super-secret-key-material");
|
|
||||||
let val = b"\x00\xFFbinary\x01\x02\x03";
|
|
||||||
|
|
||||||
let blob = f.encrypt(val);
|
|
||||||
let roundtrip = f.decrypt(&blob).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(roundtrip, val);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
That’s it: `new(secret)`, `encrypt(bytes)`, `decrypt(bytes)`.
|
|
||||||
You can stash the returned `blob` directly in your storage layer behind Redis.
|
|
@@ -1,307 +0,0 @@
|
|||||||
|
|
||||||
# 🔑 Redis `HSET` and Related Hash Commands
|
|
||||||
|
|
||||||
## 1. `HSET`
|
|
||||||
|
|
||||||
* **Purpose**: Set the value of one or more fields in a hash.
|
|
||||||
* **Syntax**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
HSET key field value [field value ...]
|
|
||||||
```
|
|
||||||
* **Return**:
|
|
||||||
|
|
||||||
* Integer: number of fields that were newly added.
|
|
||||||
* **RESP Protocol**:
|
|
||||||
|
|
||||||
```
|
|
||||||
*4
|
|
||||||
$4
|
|
||||||
HSET
|
|
||||||
$3
|
|
||||||
key
|
|
||||||
$5
|
|
||||||
field
|
|
||||||
$5
|
|
||||||
value
|
|
||||||
```
|
|
||||||
|
|
||||||
(If multiple field-value pairs: `*6`, `*8`, etc.)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. `HSETNX`
|
|
||||||
|
|
||||||
* **Purpose**: Set the value of a hash field only if it does **not** exist.
|
|
||||||
* **Syntax**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
HSETNX key field value
|
|
||||||
```
|
|
||||||
* **Return**:
|
|
||||||
|
|
||||||
* `1` if field was set.
|
|
||||||
* `0` if field already exists.
|
|
||||||
* **RESP Protocol**:
|
|
||||||
|
|
||||||
```
|
|
||||||
*4
|
|
||||||
$6
|
|
||||||
HSETNX
|
|
||||||
$3
|
|
||||||
key
|
|
||||||
$5
|
|
||||||
field
|
|
||||||
$5
|
|
||||||
value
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. `HGET`
|
|
||||||
|
|
||||||
* **Purpose**: Get the value of a hash field.
|
|
||||||
* **Syntax**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
HGET key field
|
|
||||||
```
|
|
||||||
* **Return**:
|
|
||||||
|
|
||||||
* Bulk string (value) or `nil` if field does not exist.
|
|
||||||
* **RESP Protocol**:
|
|
||||||
|
|
||||||
```
|
|
||||||
*3
|
|
||||||
$4
|
|
||||||
HGET
|
|
||||||
$3
|
|
||||||
key
|
|
||||||
$5
|
|
||||||
field
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. `HGETALL`
|
|
||||||
|
|
||||||
* **Purpose**: Get all fields and values in a hash.
|
|
||||||
* **Syntax**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
HGETALL key
|
|
||||||
```
|
|
||||||
* **Return**:
|
|
||||||
|
|
||||||
* Array of `[field1, value1, field2, value2, ...]`.
|
|
||||||
* **RESP Protocol**:
|
|
||||||
|
|
||||||
```
|
|
||||||
*2
|
|
||||||
$7
|
|
||||||
HGETALL
|
|
||||||
$3
|
|
||||||
key
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. `HMSET` (⚠️ Deprecated, use `HSET`)
|
|
||||||
|
|
||||||
* **Purpose**: Set multiple field-value pairs.
|
|
||||||
* **Syntax**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
HMSET key field value [field value ...]
|
|
||||||
```
|
|
||||||
* **Return**:
|
|
||||||
|
|
||||||
* Always `OK`.
|
|
||||||
* **RESP Protocol**:
|
|
||||||
|
|
||||||
```
|
|
||||||
*6
|
|
||||||
$5
|
|
||||||
HMSET
|
|
||||||
$3
|
|
||||||
key
|
|
||||||
$5
|
|
||||||
field
|
|
||||||
$5
|
|
||||||
value
|
|
||||||
$5
|
|
||||||
field2
|
|
||||||
$5
|
|
||||||
value2
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. `HMGET`
|
|
||||||
|
|
||||||
* **Purpose**: Get values of multiple fields.
|
|
||||||
* **Syntax**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
HMGET key field [field ...]
|
|
||||||
```
|
|
||||||
* **Return**:
|
|
||||||
|
|
||||||
* Array of values (bulk strings or nils).
|
|
||||||
* **RESP Protocol**:
|
|
||||||
|
|
||||||
```
|
|
||||||
*4
|
|
||||||
$5
|
|
||||||
HMGET
|
|
||||||
$3
|
|
||||||
key
|
|
||||||
$5
|
|
||||||
field1
|
|
||||||
$5
|
|
||||||
field2
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. `HDEL`
|
|
||||||
|
|
||||||
* **Purpose**: Delete one or more fields from a hash.
|
|
||||||
* **Syntax**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
HDEL key field [field ...]
|
|
||||||
```
|
|
||||||
* **Return**:
|
|
||||||
|
|
||||||
* Integer: number of fields removed.
|
|
||||||
* **RESP Protocol**:
|
|
||||||
|
|
||||||
```
|
|
||||||
*3
|
|
||||||
$4
|
|
||||||
HDEL
|
|
||||||
$3
|
|
||||||
key
|
|
||||||
$5
|
|
||||||
field
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. `HEXISTS`
|
|
||||||
|
|
||||||
* **Purpose**: Check if a field exists.
|
|
||||||
* **Syntax**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
HEXISTS key field
|
|
||||||
```
|
|
||||||
* **Return**:
|
|
||||||
|
|
||||||
* `1` if exists, `0` if not.
|
|
||||||
* **RESP Protocol**:
|
|
||||||
|
|
||||||
```
|
|
||||||
*3
|
|
||||||
$7
|
|
||||||
HEXISTS
|
|
||||||
$3
|
|
||||||
key
|
|
||||||
$5
|
|
||||||
field
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. `HKEYS`
|
|
||||||
|
|
||||||
* **Purpose**: Get all field names in a hash.
|
|
||||||
* **Syntax**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
HKEYS key
|
|
||||||
```
|
|
||||||
* **Return**:
|
|
||||||
|
|
||||||
* Array of field names.
|
|
||||||
* **RESP Protocol**:
|
|
||||||
|
|
||||||
```
|
|
||||||
*2
|
|
||||||
$5
|
|
||||||
HKEYS
|
|
||||||
$3
|
|
||||||
key
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. `HVALS`
|
|
||||||
|
|
||||||
* **Purpose**: Get all values in a hash.
|
|
||||||
* **Syntax**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
HVALS key
|
|
||||||
```
|
|
||||||
* **Return**:
|
|
||||||
|
|
||||||
* Array of values.
|
|
||||||
* **RESP Protocol**:
|
|
||||||
|
|
||||||
```
|
|
||||||
*2
|
|
||||||
$5
|
|
||||||
HVALS
|
|
||||||
$3
|
|
||||||
key
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. `HLEN`
|
|
||||||
|
|
||||||
* **Purpose**: Get number of fields in a hash.
|
|
||||||
* **Syntax**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
HLEN key
|
|
||||||
```
|
|
||||||
* **Return**:
|
|
||||||
|
|
||||||
* Integer: number of fields.
|
|
||||||
* **RESP Protocol**:
|
|
||||||
|
|
||||||
```
|
|
||||||
*2
|
|
||||||
$4
|
|
||||||
HLEN
|
|
||||||
$3
|
|
||||||
key
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 12. `HSCAN`
|
|
||||||
|
|
||||||
* **Purpose**: Iterate fields/values of a hash (cursor-based scan).
|
|
||||||
* **Syntax**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
HSCAN key cursor [MATCH pattern] [COUNT count]
|
|
||||||
```
|
|
||||||
* **Return**:
|
|
||||||
|
|
||||||
* Array: `[new-cursor, [field1, value1, ...]]`
|
|
||||||
* **RESP Protocol**:
|
|
||||||
|
|
||||||
```
|
|
||||||
*3
|
|
||||||
$5
|
|
||||||
HSCAN
|
|
||||||
$3
|
|
||||||
key
|
|
||||||
$1
|
|
||||||
0
|
|
||||||
```
|
|
@@ -1,80 +0,0 @@
|
|||||||
========================
|
|
||||||
CODE SNIPPETS
|
|
||||||
========================
|
|
||||||
TITLE: 1PC+C Commit Strategy Vulnerability Example
|
|
||||||
DESCRIPTION: Illustrates a scenario where a partially committed transaction might appear complete due to the non-cryptographic checksum (XXH3) used in the 1PC+C commit strategy. This requires controlling page flush order, introducing a crash during fsync, and ensuring valid checksums for partially written data.
|
|
||||||
|
|
||||||
SOURCE: https://github.com/cberner/redb/blob/master/docs/design.md#_snippet_9
|
|
||||||
|
|
||||||
LANGUAGE: rust
|
|
||||||
CODE:
|
|
||||||
```
|
|
||||||
table.insert(malicious_key, malicious_value);
|
|
||||||
table.insert(good_key, good_value);
|
|
||||||
txn.commit();
|
|
||||||
```
|
|
||||||
|
|
||||||
LANGUAGE: rust
|
|
||||||
CODE:
|
|
||||||
```
|
|
||||||
table.insert(malicious_key, malicious_value);
|
|
||||||
txn.commit();
|
|
||||||
```
|
|
||||||
|
|
||||||
----------------------------------------
|
|
||||||
|
|
||||||
TITLE: Basic Key-Value Operations in redb
|
|
||||||
DESCRIPTION: Demonstrates the fundamental usage of redb for creating a database, opening a table, inserting a key-value pair, and retrieving the value within separate read and write transactions.
|
|
||||||
|
|
||||||
SOURCE: https://github.com/cberner/redb/blob/master/README.md#_snippet_0
|
|
||||||
|
|
||||||
LANGUAGE: rust
|
|
||||||
CODE:
|
|
||||||
```
|
|
||||||
use redb::{Database, Error, ReadableTable, TableDefinition};
|
|
||||||
|
|
||||||
const TABLE: TableDefinition<&str, u64> = TableDefinition::new("my_data");
|
|
||||||
|
|
||||||
fn main() -> Result<(), Error> {
|
|
||||||
let db = Database::create("my_db.redb")?;
|
|
||||||
let write_txn = db.begin_write()?;
|
|
||||||
{
|
|
||||||
let mut table = write_txn.open_table(TABLE)?;
|
|
||||||
table.insert("my_key", &123)?;
|
|
||||||
}
|
|
||||||
write_txn.commit()?;
|
|
||||||
|
|
||||||
let read_txn = db.begin_read()?;
|
|
||||||
let table = read_txn.open_table(TABLE)?;
|
|
||||||
assert_eq!(table.get("my_key")?.unwrap().value(), 123);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## What *redb* currently supports:
|
|
||||||
|
|
||||||
* Simple operations like creating databases, inserting key-value pairs, opening and reading tables ([GitHub][1]).
|
|
||||||
* No mention of operations such as:
|
|
||||||
|
|
||||||
* Iterating over keys with a given prefix.
|
|
||||||
* Range queries based on string prefixes.
|
|
||||||
* Specialized prefix‑filtered lookups.
|
|
||||||
|
|
||||||
|
|
||||||
## implement range scans as follows
|
|
||||||
|
|
||||||
You can implement prefix-like functionality using **range scans** combined with manual checks, similar to using a `BTreeSet` in Rust:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
for key in table.range(prefix..).keys() {
|
|
||||||
if !key.starts_with(prefix) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// process key
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This pattern iterates keys starting at the prefix, and stops once a key no longer matches the prefix—this works because the keys are sorted ([GitHub][1]).
|
|
@@ -1,150 +0,0 @@
|
|||||||
]
|
|
||||||
# INFO
|
|
||||||
|
|
||||||
**What it does**
|
|
||||||
Returns server stats in a human-readable text block, optionally filtered by sections. Typical sections: `server`, `clients`, `memory`, `persistence`, `stats`, `replication`, `cpu`, `commandstats`, `latencystats`, `cluster`, `modules`, `keyspace`, `errorstats`. Special args: `all`, `default`, `everything`. The reply is a **Bulk String** with `# <Section>` headers and `key:value` lines. ([Redis][1])
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
INFO [section [section ...]]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Return (RESP2/RESP3)**: Bulk String. ([Redis][1])
|
|
||||||
|
|
||||||
**RESP request/response**
|
|
||||||
|
|
||||||
```
|
|
||||||
# Request: whole default set
|
|
||||||
*1\r\n$4\r\nINFO\r\n
|
|
||||||
|
|
||||||
# Request: a specific section, e.g., clients
|
|
||||||
*2\r\n$4\r\nINFO\r\n$7\r\nclients\r\n
|
|
||||||
|
|
||||||
# Response (prefix shown; body is long)
|
|
||||||
$1234\r\n# Server\r\nredis_version:7.4.0\r\n...\r\n# Clients\r\nconnected_clients:3\r\n...\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
(Reply type/format per RESP spec and the INFO page.) ([Redis][2])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Connection “name” (there is **no** top-level `NAME` command)
|
|
||||||
|
|
||||||
Redis doesn’t have a standalone `NAME` command. Connection names are handled via `CLIENT SETNAME` and retrieved via `CLIENT GETNAME`. ([Redis][3])
|
|
||||||
|
|
||||||
## CLIENT SETNAME
|
|
||||||
|
|
||||||
Assigns a human label to the current connection (shown in `CLIENT LIST`, logs, etc.). No spaces allowed in the name; empty string clears it. Length is limited by Redis string limits (practically huge). **Reply**: Simple String `OK`. ([Redis][4])
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
CLIENT SETNAME connection-name
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP**
|
|
||||||
|
|
||||||
```
|
|
||||||
# Set the name "myapp"
|
|
||||||
*3\r\n$6\r\nCLIENT\r\n$7\r\nSETNAME\r\n$5\r\nmyapp\r\n
|
|
||||||
|
|
||||||
# Reply
|
|
||||||
+OK\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
## CLIENT GETNAME
|
|
||||||
|
|
||||||
Returns the current connection’s name or **Null Bulk String** if unset. ([Redis][5])
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
CLIENT GETNAME
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP**
|
|
||||||
|
|
||||||
```
|
|
||||||
# Before SETNAME:
|
|
||||||
*2\r\n$6\r\nCLIENT\r\n$7\r\nGETNAME\r\n
|
|
||||||
$-1\r\n # nil (no name)
|
|
||||||
|
|
||||||
# After SETNAME myapp:
|
|
||||||
*2\r\n$6\r\nCLIENT\r\n$7\r\nGETNAME\r\n
|
|
||||||
$5\r\nmyapp\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
(Null/Bulk String encoding per RESP spec.) ([Redis][2])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# CLIENT (container command + key subcommands)
|
|
||||||
|
|
||||||
`CLIENT` is a **container**; use subcommands like `CLIENT LIST`, `CLIENT INFO`, `CLIENT ID`, `CLIENT KILL`, `CLIENT TRACKING`, etc. Call `CLIENT HELP` to enumerate them. ([Redis][3])
|
|
||||||
|
|
||||||
## CLIENT LIST
|
|
||||||
|
|
||||||
Shows all connections as a single **Bulk String**: one line per client with `field=value` pairs (includes `id`, `addr`, `name`, `db`, `user`, `resp`, and more). Filters: `TYPE` and `ID`. **Return**: Bulk String (RESP2/RESP3). ([Redis][6])
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
CLIENT LIST [TYPE <NORMAL|MASTER|REPLICA|PUBSUB>] [ID client-id ...]
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP**
|
|
||||||
|
|
||||||
```
|
|
||||||
*2\r\n$6\r\nCLIENT\r\n$4\r\nLIST\r\n
|
|
||||||
|
|
||||||
# Reply (single Bulk String; example with one line shown)
|
|
||||||
$188\r\nid=7 addr=127.0.0.1:60840 laddr=127.0.0.1:6379 fd=8 name=myapp age=12 idle=3 flags=N db=0 ...\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
## CLIENT INFO
|
|
||||||
|
|
||||||
Returns info for **this** connection only (same format/fields as a single line of `CLIENT LIST`). **Return**: Bulk String. Available since 6.2.0. ([Redis][7])
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
CLIENT INFO
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP**
|
|
||||||
|
|
||||||
```
|
|
||||||
*2\r\n$6\r\nCLIENT\r\n$4\r\nINFO\r\n
|
|
||||||
|
|
||||||
$160\r\nid=7 addr=127.0.0.1:60840 laddr=127.0.0.1:6379 fd=8 name=myapp db=0 user=default resp=2 ...\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# RESP notes you’ll need for your parser
|
|
||||||
|
|
||||||
* **Requests** are Arrays: `*N\r\n` followed by `N` Bulk Strings for verb/args.
|
|
||||||
* **Common replies here**: Simple String (`+OK\r\n`), Bulk String (`$<len>\r\n...\r\n`), and **Null Bulk String** (`$-1\r\n`). (These cover `INFO`, `CLIENT LIST/INFO`, `CLIENT GETNAME`, `CLIENT SETNAME`.) ([Redis][2])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sources (checked)
|
|
||||||
|
|
||||||
* INFO command (syntax, sections, behavior). ([Redis][1])
|
|
||||||
* RESP spec (request/response framing, Bulk/Null Bulk Strings). ([Redis][2])
|
|
||||||
* CLIENT container + subcommands index. ([Redis][3])
|
|
||||||
* CLIENT LIST (fields, bulk-string return, filters). ([Redis][6])
|
|
||||||
* CLIENT INFO (exists since 6.2, reply format). ([Redis][7])
|
|
||||||
* CLIENT SETNAME (no spaces; clears with empty string; huge length OK). ([Redis][4])
|
|
||||||
* CLIENT GETNAME (nil if unset). ([Redis][5])
|
|
||||||
|
|
||||||
If you want, I can fold this into a tiny Rust “command + RESP” test harness that exercises `INFO`, `CLIENT SETNAME/GETNAME`, `CLIENT LIST`, and `CLIENT INFO` against your in-mem RESP parser.
|
|
||||||
|
|
||||||
[1]: https://redis.io/docs/latest/commands/info/ "INFO | Docs"
|
|
||||||
[2]: https://redis.io/docs/latest/develop/reference/protocol-spec/?utm_source=chatgpt.com "Redis serialization protocol specification | Docs"
|
|
||||||
[3]: https://redis.io/docs/latest/commands/client/ "CLIENT | Docs"
|
|
||||||
[4]: https://redis.io/docs/latest/commands/client-setname/?utm_source=chatgpt.com "CLIENT SETNAME | Docs"
|
|
||||||
[5]: https://redis.io/docs/latest/commands/client-getname/?utm_source=chatgpt.com "CLIENT GETNAME | Docs"
|
|
||||||
[6]: https://redis.io/docs/latest/commands/client-list/ "CLIENT LIST | Docs"
|
|
||||||
[7]: https://redis.io/docs/latest/commands/client-info/?utm_source=chatgpt.com "CLIENT INFO | Docs"
|
|
@@ -1,251 +0,0 @@
|
|||||||
Got it 👍 — let’s break this down properly.
|
|
||||||
|
|
||||||
Redis has two broad classes you’re asking about:
|
|
||||||
|
|
||||||
1. **Basic key-space functions** (SET, GET, DEL, EXISTS, etc.)
|
|
||||||
2. **Iteration commands** (`SCAN`, `SSCAN`, `HSCAN`, `ZSCAN`)
|
|
||||||
|
|
||||||
And for each I’ll show:
|
|
||||||
|
|
||||||
* What it does
|
|
||||||
* How it works at a high level
|
|
||||||
* Its **RESP protocol implementation** (the actual wire format).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 1. Basic Key-Space Commands
|
|
||||||
|
|
||||||
### `SET key value`
|
|
||||||
|
|
||||||
* Stores a string value at a key.
|
|
||||||
* Overwrites if the key already exists.
|
|
||||||
|
|
||||||
**Protocol (RESP2):**
|
|
||||||
|
|
||||||
```
|
|
||||||
*3
|
|
||||||
$3
|
|
||||||
SET
|
|
||||||
$3
|
|
||||||
foo
|
|
||||||
$3
|
|
||||||
bar
|
|
||||||
```
|
|
||||||
|
|
||||||
(client sends: array of 3 bulk strings: `["SET", "foo", "bar"]`)
|
|
||||||
|
|
||||||
**Reply:**
|
|
||||||
|
|
||||||
```
|
|
||||||
+OK
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `GET key`
|
|
||||||
|
|
||||||
* Retrieves the string value stored at the key.
|
|
||||||
* Returns `nil` if key doesn’t exist.
|
|
||||||
|
|
||||||
**Protocol:**
|
|
||||||
|
|
||||||
```
|
|
||||||
*2
|
|
||||||
$3
|
|
||||||
GET
|
|
||||||
$3
|
|
||||||
foo
|
|
||||||
```
|
|
||||||
|
|
||||||
**Reply:**
|
|
||||||
|
|
||||||
```
|
|
||||||
$3
|
|
||||||
bar
|
|
||||||
```
|
|
||||||
|
|
||||||
(or `$-1` for nil)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `DEL key [key ...]`
|
|
||||||
|
|
||||||
* Removes one or more keys.
|
|
||||||
* Returns number of keys actually removed.
|
|
||||||
|
|
||||||
**Protocol:**
|
|
||||||
|
|
||||||
```
|
|
||||||
*2
|
|
||||||
$3
|
|
||||||
DEL
|
|
||||||
$3
|
|
||||||
foo
|
|
||||||
```
|
|
||||||
|
|
||||||
**Reply:**
|
|
||||||
|
|
||||||
```
|
|
||||||
:1
|
|
||||||
```
|
|
||||||
|
|
||||||
(integer reply = number of deleted keys)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `EXISTS key [key ...]`
|
|
||||||
|
|
||||||
* Checks if one or more keys exist.
|
|
||||||
* Returns count of existing keys.
|
|
||||||
|
|
||||||
**Protocol:**
|
|
||||||
|
|
||||||
```
|
|
||||||
*2
|
|
||||||
$6
|
|
||||||
EXISTS
|
|
||||||
$3
|
|
||||||
foo
|
|
||||||
```
|
|
||||||
|
|
||||||
**Reply:**
|
|
||||||
|
|
||||||
```
|
|
||||||
:1
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `KEYS pattern`
|
|
||||||
|
|
||||||
* Returns all keys matching a glob-style pattern.
|
|
||||||
⚠️ Not efficient in production (O(N)), better to use `SCAN`.
|
|
||||||
|
|
||||||
**Protocol:**
|
|
||||||
|
|
||||||
```
|
|
||||||
*2
|
|
||||||
$4
|
|
||||||
KEYS
|
|
||||||
$1
|
|
||||||
*
|
|
||||||
```
|
|
||||||
|
|
||||||
**Reply:**
|
|
||||||
|
|
||||||
```
|
|
||||||
*2
|
|
||||||
$3
|
|
||||||
foo
|
|
||||||
$3
|
|
||||||
bar
|
|
||||||
```
|
|
||||||
|
|
||||||
(array of bulk strings with key names)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 2. Iteration Commands (`SCAN` family)
|
|
||||||
|
|
||||||
### `SCAN cursor [MATCH pattern] [COUNT n]`
|
|
||||||
|
|
||||||
* Iterates the keyspace incrementally.
|
|
||||||
* Client keeps sending back the cursor from previous call until it returns `0`.
|
|
||||||
|
|
||||||
**Protocol example:**
|
|
||||||
|
|
||||||
```
|
|
||||||
*2
|
|
||||||
$4
|
|
||||||
SCAN
|
|
||||||
$1
|
|
||||||
0
|
|
||||||
```
|
|
||||||
|
|
||||||
**Reply:**
|
|
||||||
|
|
||||||
```
|
|
||||||
*2
|
|
||||||
$1
|
|
||||||
0
|
|
||||||
*2
|
|
||||||
$3
|
|
||||||
foo
|
|
||||||
$3
|
|
||||||
bar
|
|
||||||
```
|
|
||||||
|
|
||||||
Explanation:
|
|
||||||
|
|
||||||
* First element = new cursor (`"0"` means iteration finished).
|
|
||||||
* Second element = array of keys returned in this batch.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `HSCAN key cursor [MATCH pattern] [COUNT n]`
|
|
||||||
|
|
||||||
* Like `SCAN`, but iterates fields of a hash.
|
|
||||||
|
|
||||||
**Protocol:**
|
|
||||||
|
|
||||||
```
|
|
||||||
*3
|
|
||||||
$5
|
|
||||||
HSCAN
|
|
||||||
$3
|
|
||||||
myh
|
|
||||||
$1
|
|
||||||
0
|
|
||||||
```
|
|
||||||
|
|
||||||
**Reply:**
|
|
||||||
|
|
||||||
```
|
|
||||||
*2
|
|
||||||
$1
|
|
||||||
0
|
|
||||||
*4
|
|
||||||
$5
|
|
||||||
field
|
|
||||||
$5
|
|
||||||
value
|
|
||||||
$5
|
|
||||||
age
|
|
||||||
$2
|
|
||||||
42
|
|
||||||
```
|
|
||||||
|
|
||||||
(Array of alternating field/value pairs)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `SSCAN key cursor [MATCH pattern] [COUNT n]`
|
|
||||||
|
|
||||||
* Iterates members of a set.
|
|
||||||
|
|
||||||
Protocol and reply structure same as SCAN.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `ZSCAN key cursor [MATCH pattern] [COUNT n]`
|
|
||||||
|
|
||||||
* Iterates members of a sorted set with scores.
|
|
||||||
* Returns alternating `member`, `score`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Quick Comparison
|
|
||||||
|
|
||||||
| Command | Purpose | Return Type |
|
|
||||||
| -------- | ----------------------------- | --------------------- |
|
|
||||||
| `SET` | Store a string value | Simple string `+OK` |
|
|
||||||
| `GET` | Retrieve a string value | Bulk string / nil |
|
|
||||||
| `DEL` | Delete keys | Integer (count) |
|
|
||||||
| `EXISTS` | Check existence | Integer (count) |
|
|
||||||
| `KEYS` | List all matching keys (slow) | Array of bulk strings |
|
|
||||||
| `SCAN` | Iterate over keys (safe) | `[cursor, array]` |
|
|
||||||
| `HSCAN` | Iterate over hash fields | `[cursor, array]` |
|
|
||||||
| `SSCAN` | Iterate over set members | `[cursor, array]` |
|
|
||||||
| `ZSCAN` | Iterate over sorted set | `[cursor, array]` |
|
|
||||||
|
|
||||||
##
|
|
@@ -1,259 +0,0 @@
|
|||||||
|
|
||||||
# 1) Data model & basics
|
|
||||||
|
|
||||||
* A **queue** is a List at key `queue:<name>`.
|
|
||||||
* Common patterns:
|
|
||||||
|
|
||||||
* **Producer**: `LPUSH queue item` (or `RPUSH`)
|
|
||||||
* **Consumer (non-blocking)**: `RPOP queue` (or `LPOP`)
|
|
||||||
* **Consumer (blocking)**: `BRPOP queue timeout` (or `BLPOP`)
|
|
||||||
* If a key doesn’t exist, it’s treated as an **empty list**; push **creates** the list; when the **last element is popped, the key is deleted**. ([Redis][1])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 2) Commands to implement (queues via Lists)
|
|
||||||
|
|
||||||
## LPUSH / RPUSH
|
|
||||||
|
|
||||||
Prepend/append one or more elements. Create the list if it doesn’t exist.
|
|
||||||
**Return**: Integer = new length of the list.
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
LPUSH key element [element ...]
|
|
||||||
RPUSH key element [element ...]
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP (example)**
|
|
||||||
|
|
||||||
```
|
|
||||||
*3\r\n$5\r\nLPUSH\r\n$5\r\nqueue\r\n$5\r\njob-1\r\n
|
|
||||||
:1\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
Refs: semantics & multi-arg ordering. ([Redis][1])
|
|
||||||
|
|
||||||
### LPUSHX / RPUSHX (optional but useful)
|
|
||||||
|
|
||||||
Like LPUSH/RPUSH, **but only if the list exists**.
|
|
||||||
**Return**: Integer = new length (0 if key didn’t exist).
|
|
||||||
|
|
||||||
```
|
|
||||||
LPUSHX key element [element ...]
|
|
||||||
RPUSHX key element [element ...]
|
|
||||||
```
|
|
||||||
|
|
||||||
Refs: command index. ([Redis][2])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LPOP / RPOP
|
|
||||||
|
|
||||||
Remove & return one (default) or **up to COUNT** elements since Redis 6.2.
|
|
||||||
If the list is empty or missing, **Null** is returned (Null Bulk or Null Array if COUNT>1).
|
|
||||||
**Return**:
|
|
||||||
|
|
||||||
* No COUNT: Bulk String or Null Bulk.
|
|
||||||
* With COUNT: Array of Bulk Strings (possibly empty) or Null Array if key missing.
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
LPOP key [count]
|
|
||||||
RPOP key [count]
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP (no COUNT)**
|
|
||||||
|
|
||||||
```
|
|
||||||
*2\r\n$4\r\nRPOP\r\n$5\r\nqueue\r\n
|
|
||||||
$5\r\njob-1\r\n # or $-1\r\n if empty
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP (COUNT=2)**
|
|
||||||
|
|
||||||
```
|
|
||||||
*3\r\n$4\r\nLPOP\r\n$5\r\nqueue\r\n$1\r\n2\r\n
|
|
||||||
*2\r\n$5\r\njob-2\r\n$5\r\njob-3\r\n # or *-1\r\n if key missing
|
|
||||||
```
|
|
||||||
|
|
||||||
Refs: LPOP w/ COUNT; general pop semantics. ([Redis][3])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## BLPOP / BRPOP (blocking consumers)
|
|
||||||
|
|
||||||
Block until an element is available in any of the given lists or until `timeout` (seconds, **double**, `0` = forever).
|
|
||||||
**Return** on success: **Array \[key, element]**.
|
|
||||||
**Return** on timeout: **Null Array**.
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
BLPOP key [key ...] timeout
|
|
||||||
BRPOP key [key ...] timeout
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP**
|
|
||||||
|
|
||||||
```
|
|
||||||
*3\r\n$5\r\nBRPOP\r\n$5\r\nqueue\r\n$1\r\n0\r\n # block forever
|
|
||||||
|
|
||||||
# Success reply
|
|
||||||
*2\r\n$5\r\nqueue\r\n$5\r\njob-4\r\n
|
|
||||||
|
|
||||||
# Timeout reply
|
|
||||||
*-1\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
**Implementation notes**
|
|
||||||
|
|
||||||
* If any listed key is non-empty at call time, reply **immediately** from the first non-empty key **by the command’s key order**.
|
|
||||||
* Otherwise, put the client into a **blocked state** (register per-key waiters). On any `LPUSH/RPUSH` to those keys, **wake the earliest waiter** and serve it atomically.
|
|
||||||
* If timeout expires, return **Null Array** and clear the blocked state.
|
|
||||||
Refs: timeout semantics and return shape. ([Redis][4])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LMOVE / BLMOVE (atomic move; replaces RPOPLPUSH/BRPOPLPUSH)
|
|
||||||
|
|
||||||
Atomically **pop from one side** of `source` and **push to one side** of `destination`.
|
|
||||||
|
|
||||||
* Use for **reliable queues** (move to a *processing* list).
|
|
||||||
* `BLMOVE` blocks like `BLPOP` when `source` is empty.
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
LMOVE source destination LEFT|RIGHT LEFT|RIGHT
|
|
||||||
BLMOVE source destination LEFT|RIGHT LEFT|RIGHT timeout
|
|
||||||
```
|
|
||||||
|
|
||||||
**Return**: Bulk String element moved, or Null if `source` empty (LMOVE); `BLMOVE` blocks/Null on timeout.
|
|
||||||
|
|
||||||
**RESP (LMOVE RIGHT->LEFT)**
|
|
||||||
|
|
||||||
```
|
|
||||||
*5\r\n$5\r\nLMOVE\r\n$6\r\nsource\r\n$3\r\ndst\r\n$5\r\nRIGHT\r\n$4\r\nLEFT\r\n
|
|
||||||
$5\r\njob-5\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
**Notes**
|
|
||||||
|
|
||||||
* Prefer `LMOVE/BLMOVE` over deprecated `RPOPLPUSH/BRPOPLPUSH`.
|
|
||||||
* Pattern: consumer `LMOVE queue processing RIGHT LEFT` → work → `LREM processing 1 <elem>` to ACK; a reaper can requeue stale items.
|
|
||||||
Refs: LMOVE/BLMOVE behavior and reliable-queue pattern; deprecation of RPOPLPUSH. ([Redis][5])
|
|
||||||
|
|
||||||
*(Compat: you can still implement `RPOPLPUSH source dest` and `BRPOPLPUSH source dest timeout`, but mark them deprecated and map to LMOVE/BLMOVE.)* ([Redis][6])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LLEN (length)
|
|
||||||
|
|
||||||
Useful for metrics/backpressure.
|
|
||||||
|
|
||||||
```
|
|
||||||
LLEN key
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP**
|
|
||||||
|
|
||||||
```
|
|
||||||
*2\r\n$4\r\nLLEN\r\n$5\r\nqueue\r\n
|
|
||||||
:3\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
Refs: list overview mentioning LLEN. ([Redis][7])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LREM (ack for “reliable” processing)
|
|
||||||
|
|
||||||
Remove occurrences of `element` from the list (head→tail scan).
|
|
||||||
Use `count=1` to ACK a single processed item from `processing`.
|
|
||||||
|
|
||||||
```
|
|
||||||
LREM key count element
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP**
|
|
||||||
|
|
||||||
```
|
|
||||||
*4\r\n$4\r\nLREM\r\n$9\r\nprocessing\r\n$1\r\n1\r\n$5\r\njob-5\r\n
|
|
||||||
:1\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
Refs: reliable pattern mentions LREM to ACK. ([Redis][5])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LTRIM (bounded queues / retention)
|
|
||||||
|
|
||||||
Keep only `[start, stop]` range; everything else is dropped.
|
|
||||||
Use to cap queue length after pushes.
|
|
||||||
|
|
||||||
```
|
|
||||||
LTRIM key start stop
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP**
|
|
||||||
|
|
||||||
```
|
|
||||||
*4\r\n$5\r\nLTRIM\r\n$5\r\nqueue\r\n$2\r\n0\r\n$3\r\n999\r\n
|
|
||||||
+OK\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
Refs: list overview includes LTRIM for retention. ([Redis][7])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LRANGE / LINDEX (debugging / peeking)
|
|
||||||
|
|
||||||
* `LRANGE key start stop` → Array of elements (non-destructive).
|
|
||||||
* `LINDEX key index` → one element or Null.
|
|
||||||
|
|
||||||
These aren’t required for queue semantics, but handy. ([Redis][7])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 3) Errors & types
|
|
||||||
|
|
||||||
* Wrong type: `-WRONGTYPE Operation against a key holding the wrong kind of value\r\n`
|
|
||||||
* Non-existing key:
|
|
||||||
|
|
||||||
* Push: creates the list (returns new length).
|
|
||||||
* Pop (non-blocking): returns **Null**.
|
|
||||||
* Blocking pop: **Null Array** on timeout. ([Redis][1])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 4) Blocking engine (implementation sketch)
|
|
||||||
|
|
||||||
1. **Call time**: scan keys in user order. If a non-empty list is found, pop & reply immediately.
|
|
||||||
2. **Otherwise**: register the client as **blocked** on those keys with `deadline = now + timeout` (or infinite).
|
|
||||||
3. **On push to any key**: if waiters exist, **wake one** (FIFO) and serve its pop **atomically** with the push result.
|
|
||||||
4. **On timer**: for each blocked client whose deadline passed, reply `Null Array` and clear state.
|
|
||||||
5. **Connection close**: remove from any wait queues.
|
|
||||||
|
|
||||||
Refs for timeout/block semantics. ([Redis][4])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 5) Reliable queue pattern (recommended)
|
|
||||||
|
|
||||||
* **Consume**: `LMOVE queue processing RIGHT LEFT` (or `BLMOVE ... 0`).
|
|
||||||
* **Process** the job.
|
|
||||||
* **ACK**: `LREM processing 1 <job>` when done.
|
|
||||||
* **Reaper**: auxiliary task that detects stale jobs (e.g., track job IDs + timestamps in a ZSET) and requeues them. (Lists don’t include timestamps; pairing with a ZSET is standard practice.)
|
|
||||||
Refs: LMOVE doc’s pattern. ([Redis][5])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 6) Minimal test matrix
|
|
||||||
|
|
||||||
* Push/pop happy path (both ends), with/without COUNT.
|
|
||||||
* Blocking pop: immediate availability, block + timeout, wake on push, multiple keys order, FIFO across multiple waiters.
|
|
||||||
* LMOVE/BLMOVE: RIGHT→LEFT pipeline, block + wake, cross-list atomicity, ACK via LREM.
|
|
||||||
* Type errors and key deletion on last pop.
|
|
||||||
|
|
16
run_tests.sh
16
run_tests.sh
@@ -6,20 +6,20 @@ echo "=========================================="
|
|||||||
echo ""
|
echo ""
|
||||||
echo "1️⃣ Running Simple Redis Tests (4 tests)..."
|
echo "1️⃣ Running Simple Redis Tests (4 tests)..."
|
||||||
echo "----------------------------------------------"
|
echo "----------------------------------------------"
|
||||||
cargo test --test simple_redis_test -- --nocapture
|
cargo test -p herodb --test simple_redis_test -- --nocapture
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "2️⃣ Running Comprehensive Redis Integration Tests (13 tests)..."
|
echo "2️⃣ Running Comprehensive Redis Integration Tests (13 tests)..."
|
||||||
echo "----------------------------------------------------------------"
|
echo "----------------------------------------------------------------"
|
||||||
cargo test --test redis_integration_tests -- --nocapture
|
cargo test -p herodb --test redis_integration_tests -- --nocapture
|
||||||
cargo test --test redis_basic_client -- --nocapture
|
cargo test -p herodb --test redis_basic_client -- --nocapture
|
||||||
cargo test --test debug_hset -- --nocapture
|
cargo test -p herodb --test debug_hset -- --nocapture
|
||||||
cargo test --test debug_hset_simple -- --nocapture
|
cargo test -p herodb --test debug_hset_simple -- --nocapture
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "3️⃣ Running All Tests..."
|
echo "3️⃣ Running All Workspace Tests..."
|
||||||
echo "------------------------"
|
echo "--------------------------------"
|
||||||
cargo test -- --nocapture
|
cargo test --workspace -- --nocapture
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ Test execution completed!"
|
echo "✅ Test execution completed!"
|
@@ -1,7 +0,0 @@
|
|||||||
pub mod cmd;
|
|
||||||
pub mod crypto;
|
|
||||||
pub mod error;
|
|
||||||
pub mod options;
|
|
||||||
pub mod protocol;
|
|
||||||
pub mod server;
|
|
||||||
pub mod storage;
|
|
1261
src/storage.rs
1261
src/storage.rs
File diff suppressed because it is too large
Load Diff
@@ -288,7 +288,7 @@ main() {
|
|||||||
|
|
||||||
# Build the project
|
# Build the project
|
||||||
print_status "Building HeroDB..."
|
print_status "Building HeroDB..."
|
||||||
if ! cargo build --release; then
|
if ! cargo build -p herodb --release; then
|
||||||
print_error "Failed to build HeroDB"
|
print_error "Failed to build HeroDB"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -298,7 +298,7 @@ main() {
|
|||||||
|
|
||||||
# Start the server
|
# Start the server
|
||||||
print_status "Starting HeroDB server..."
|
print_status "Starting HeroDB server..."
|
||||||
./target/release/redis-rs --dir "$DB_DIR" --port $PORT &
|
./target/release/herodb --dir "$DB_DIR" --port $PORT &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
|
|
||||||
# Wait for server to start
|
# Wait for server to start
|
||||||
|
Reference in New Issue
Block a user