use geth_types::{KvId, ResourceId}; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct KvResource { pub id: KvId, pub resource: ResourceId, pub name: String, pub sync_status: String, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct KvEntry { pub store: KvId, pub key: String, pub value: String, } #[derive(Debug, thiserror::Error)] pub enum KvError { #[error("invalid kv store name: {0}")] InvalidName(String), #[error("invalid kv key: {0}")] InvalidKey(String), } pub fn validate_kv_name(name: &str) -> Result<(), KvError> { if name.is_empty() || !name .bytes() .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')) { return Err(KvError::InvalidName(name.to_owned())); } Ok(()) } pub fn validate_kv_key(key: &str) -> Result<(), KvError> { if key.is_empty() || key.bytes().any(|byte| byte == 0 || byte == b'\n') { return Err(KvError::InvalidKey(key.to_owned())); } Ok(()) } #[must_use] pub fn iroh_docs_roadmap() -> &'static str { "future kv storage uses Iroh Documents namespaces with prefix-scoped authorization" } #[cfg(test)] mod tests { use super::*; #[test] fn kv_name_validation_rejects_paths_and_empty_names() { assert!(validate_kv_name("prefs").is_ok()); assert!(validate_kv_name("apps.foo").is_ok()); assert!(validate_kv_name("").is_err()); assert!(validate_kv_name("../prefs").is_err()); assert!(validate_kv_name("prefs/main").is_err()); assert!(validate_kv_name("prefs main").is_err()); } #[test] fn kv_key_validation_rejects_empty_newline_and_nul() { assert!(validate_kv_key("apps/foo/theme").is_ok()); assert!(validate_kv_key("").is_err()); assert!(validate_kv_key("apps/foo\nbar").is_err()); assert!(validate_kv_key("apps/foo\0bar").is_err()); } }