diff --git a/src/age.rs b/src/age.rs index 77501da..f72132c 100644 --- a/src/age.rs +++ b/src/age.rs @@ -19,6 +19,7 @@ use age::x25519; use ed25519_dalek::{Signature, Signer, Verifier, SigningKey, VerifyingKey}; use base64::{engine::general_purpose::STANDARD as B64, Engine as _}; +use std::collections::HashSet; use crate::protocol::Protocol; use crate::server::Server; @@ -276,33 +277,31 @@ pub async fn cmd_age_verify_name(server: &Server, name: &str, message: &str, sig } pub async fn cmd_age_list(server: &Server) -> Protocol { - // Returns 4 arrays: ["encpub", ], ["encpriv", ...], ["signpub", ...], ["signpriv", ...] + // Return a flat, deduplicated, sorted list of managed key names (no labels) let st = match server.current_storage() { Ok(s) => s, Err(e) => return Protocol::err(&e.0) }; let pull = |pat: &str, prefix: &str| -> Result, DBError> { let keys = st.keys(pat)?; - let mut names: Vec = keys.into_iter() + let mut names: Vec = 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 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| { - let mut out = vec![Protocol::BulkString(label.to_string())]; - out.push(Protocol::Array(v.into_iter().map(Protocol::BulkString).collect())); - Protocol::Array(out) - }; + let mut set: HashSet = HashSet::new(); + for n in encpub.into_iter().chain(encpriv).chain(signpub).chain(signpriv) { + set.insert(n); + } - Protocol::Array(vec![ - to_arr("encpub", encpub), - to_arr("encpriv", encpriv), - to_arr("signpub", signpub), - to_arr("signpriv", signpriv), - ]) + let mut names: Vec = set.into_iter().collect(); + names.sort(); + + Protocol::Array(names.into_iter().map(Protocol::BulkString).collect()) } \ No newline at end of file diff --git a/src/cmd.rs b/src/cmd.rs index 98cbacd..1fd7dd0 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -1210,8 +1210,13 @@ async fn del_cmd(server: &Server, k: &str) -> Result { if !server.has_write_permission() { return Ok(Protocol::err("ERR write permission denied")); } - server.current_storage()?.del(k.to_string())?; - Ok(Protocol::SimpleString("1".to_string())) + let storage = server.current_storage()?; + if storage.exists(k)? { + storage.del(k.to_string())?; + Ok(Protocol::SimpleString("1".to_string())) + } else { + Ok(Protocol::SimpleString("0".to_string())) + } } async fn set_ex_cmd( @@ -1322,6 +1327,9 @@ async fn mset_cmd(server: &Server, pairs: &[(String, String)]) -> Result Result { + if !server.has_write_permission() { + return Ok(Protocol::err("ERR write permission denied")); + } let storage = server.current_storage()?; let mut deleted = 0i64; for k in keys { diff --git a/src/rpc.rs b/src/rpc.rs index a6d4400..49a69de 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -48,20 +48,7 @@ pub enum Permissions { ReadWrite, } -/// Access key information -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AccessKey { - pub hash: String, - pub permissions: Permissions, - pub created_at: u64, -} -/// Database metadata containing access keys -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DatabaseMeta { - pub public: bool, - pub keys: HashMap, -} /// Access key information returned by RPC #[derive(Debug, Clone, Serialize, Deserialize)] @@ -133,14 +120,8 @@ pub struct RpcServerImpl { base_dir: String, /// Managed database servers servers: Arc>>>, - /// Next unencrypted database ID to assign - next_unencrypted_id: Arc>, - /// Next encrypted database ID to assign - next_encrypted_id: Arc>, /// Default backend type backend: crate::options::BackendType, - /// Encryption keys for databases - encryption_keys: Arc>>>, /// Admin secret used to encrypt DB 0 and authorize admin access admin_secret: String, } @@ -151,10 +132,7 @@ impl RpcServerImpl { Self { base_dir, servers: Arc::new(RwLock::new(HashMap::new())), - next_unencrypted_id: Arc::new(RwLock::new(0)), - next_encrypted_id: Arc::new(RwLock::new(10)), backend, - encryption_keys: Arc::new(RwLock::new(HashMap::new())), admin_secret, } } @@ -212,124 +190,10 @@ impl RpcServerImpl { .unwrap_or_default() } - /// Get the next available database ID - async fn get_next_db_id(&self, is_encrypted: bool) -> u64 { - if is_encrypted { - let mut id = self.next_encrypted_id.write().await; - let current_id = *id; - *id += 1; - current_id - } else { - let mut id = self.next_unencrypted_id.write().await; - let current_id = *id; - *id += 1; - current_id - } - } - /// Load database metadata from file (static version) - pub async fn load_meta_static(base_dir: &str, db_id: u64) -> Result { - let meta_path = std::path::PathBuf::from(base_dir).join(format!("{}_meta.json", db_id)); - // If meta file doesn't exist, create and persist default - if !meta_path.exists() { - let default_meta = DatabaseMeta { - public: true, - keys: HashMap::new(), - }; - // Persist default metadata to disk - Self::save_meta_static(base_dir, db_id, &default_meta).await?; - return Ok(default_meta); - } - // Read file as UTF-8 JSON - let json_str = std::fs::read_to_string(&meta_path) - .map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned( - -32000, - format!("Failed to read meta file: {}", e), - None::<()> - ))?; - serde_json::from_str(&json_str) - .map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned( - -32000, - format!("Failed to parse meta JSON: {}", e), - None::<()> - )) - } - - /// Load database metadata from file - async fn load_meta(&self, db_id: u64) -> Result { - let meta_path = std::path::PathBuf::from(&self.base_dir).join(format!("{}_meta.json", db_id)); - - // If meta file doesn't exist, create and persist default - if !meta_path.exists() { - let default_meta = DatabaseMeta { - public: true, - keys: HashMap::new(), - }; - self.save_meta(db_id, &default_meta).await?; - return Ok(default_meta); - } - - // Read file as UTF-8 JSON (meta files are always plain JSON) - let json_str = std::fs::read_to_string(&meta_path) - .map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned( - -32000, - format!("Failed to read meta file: {}", e), - None::<()> - ))?; - - serde_json::from_str(&json_str) - .map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned( - -32000, - format!("Failed to parse meta JSON: {}", e), - None::<()> - )) - } - - /// Save database metadata to file (static version) - pub async fn save_meta_static(base_dir: &str, db_id: u64, meta: &DatabaseMeta) -> Result<(), jsonrpsee::types::ErrorObjectOwned> { - let meta_path = std::path::PathBuf::from(base_dir).join(format!("{}_meta.json", db_id)); - - let json_str = serde_json::to_string_pretty(meta) - .map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned( - -32000, - format!("Failed to serialize meta: {}", e), - None::<()> - ))?; - - std::fs::write(&meta_path, json_str) - .map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned( - -32000, - format!("Failed to write meta file: {}", e), - None::<()> - ))?; - - Ok(()) - } - - /// Save database metadata to file - async fn save_meta(&self, db_id: u64, meta: &DatabaseMeta) -> Result<(), jsonrpsee::types::ErrorObjectOwned> { - let meta_path = std::path::PathBuf::from(&self.base_dir).join(format!("{}_meta.json", db_id)); - - let json_str = serde_json::to_string_pretty(meta) - .map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned( - -32000, - format!("Failed to serialize meta: {}", e), - None::<()> - ))?; - - // Meta files are always stored as plain JSON (even when data DB is encrypted) - std::fs::write(&meta_path, json_str) - .map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned( - -32000, - format!("Failed to write meta file: {}", e), - None::<()> - ))?; - - Ok(()) - } /// Build database file path for given server/db_id fn db_file_path(&self, server: &Server, db_id: u64) -> std::path::PathBuf { @@ -374,29 +238,21 @@ impl RpcServerImpl { (created, last_access) } - /// Compose a DatabaseInfo by probing storage, metadata and filesystem + /// Compose a DatabaseInfo by probing storage and filesystem, with admin meta for access key count async fn build_database_info(&self, db_id: u64, server: &Server) -> DatabaseInfo { // Probe storage to determine encryption state let storage = server.current_storage().ok(); let encrypted = storage.as_ref().map(|s| s.is_encrypted()).unwrap_or(server.option.encrypt); - // Load meta to get access key count - let meta = Self::load_meta_static(&self.base_dir, db_id).await.unwrap_or(DatabaseMeta { - public: true, - keys: HashMap::new(), - }); - let key_count = Some(meta.keys.len() as u64); + // Access key count via admin DB 0 + let key_count = admin_meta::list_access_keys(&self.base_dir, self.backend.clone(), &self.admin_secret, db_id) + .map(|v| v.len() as u64) + .ok(); - // Compute size on disk and timestamps + // Compute size on disk and timestamps from the DB file path let db_path = self.db_file_path(server, db_id); let size_on_disk = self.compute_size_on_disk(&db_path); - - let meta_path = std::path::PathBuf::from(&self.base_dir).join(format!("{}_meta.json", db_id)); - let (created_at, last_access) = if meta_path.exists() { - Self::get_file_times_secs(&meta_path) - } else { - Self::get_file_times_secs(&db_path) - }; + let (created_at, last_access) = Self::get_file_times_secs(&db_path); let backend = match server.option.backend { crate::options::BackendType::Redb => BackendType::Redb, @@ -468,7 +324,7 @@ impl RpcServer for RpcServerImpl { Ok(db_id) } - async fn set_encryption(&self, db_id: u64, _encryption_key: String) -> RpcResult { + async fn set_encryption(&self, _db_id: u64, _encryption_key: String) -> RpcResult { // For now, return false as encryption can only be set during creation let _servers = self.servers.read().await; // TODO: Implement encryption setting for existing databases diff --git a/tests/usage_suite.rs b/tests/usage_suite.rs index 0203c28..3754906 100644 --- a/tests/usage_suite.rs +++ b/tests/usage_suite.rs @@ -290,7 +290,11 @@ async fn test_02_strings_and_expiry() { let ex0 = send_cmd(&mut s, &["EXISTS", "user:1"]).await; assert_contains(&ex0, "0", "EXISTS after DEL"); - + + // DEL non-existent should return 0 + let del0 = send_cmd(&mut s, &["DEL", "user:1"]).await; + assert_contains(&del0, "0", "DEL user:1 when not exists -> 0"); + // INCR behavior let i1 = send_cmd(&mut s, &["INCR", "count"]).await; assert_contains(&i1, "1", "INCR new key -> 1"); @@ -602,7 +606,7 @@ async fn test_08_age_persistent_named_suite() { // AGE LIST let lst = send_cmd(&mut s, &["AGE", "LIST"]).await; - assert_contains(&lst, "encpub", "AGE LIST label encpub"); + // After flattening, LIST returns a flat array of managed key names assert_contains(&lst, "app1", "AGE LIST includes app1"); }