turbovault_plugin_api/
storage.rs1use std::sync::Arc;
2
3use async_trait::async_trait;
4
5use crate::{PluginError, PluginResult};
6
7#[async_trait]
13pub trait PluginStore: Send + Sync {
14 async fn get(&self, vault: &str, key: &str) -> PluginResult<Option<Vec<u8>>>;
16
17 async fn put(&self, vault: &str, key: &str, value: &[u8]) -> PluginResult<()>;
19
20 async fn delete(&self, vault: &str, key: &str) -> PluginResult<()>;
22
23 async fn list(&self, vault: &str, prefix: &str) -> PluginResult<Vec<String>>;
25}
26
27#[derive(Clone)]
43pub struct PluginStorage {
44 store: Arc<dyn PluginStore>,
45}
46
47impl std::fmt::Debug for PluginStorage {
48 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 formatter
50 .debug_struct("PluginStorage")
51 .finish_non_exhaustive()
52 }
53}
54
55impl PluginStorage {
56 pub fn new(store: Arc<dyn PluginStore>) -> Self {
58 Self { store }
59 }
60
61 pub async fn get(&self, vault: &str, key: &str) -> PluginResult<Option<Vec<u8>>> {
63 self.store.get(vault, &validate_key(key)?).await
64 }
65
66 pub async fn put(&self, vault: &str, key: &str, value: &[u8]) -> PluginResult<()> {
68 self.store.put(vault, &validate_key(key)?, value).await
69 }
70
71 pub async fn delete(&self, vault: &str, key: &str) -> PluginResult<()> {
73 self.store.delete(vault, &validate_key(key)?).await
74 }
75
76 pub async fn list(&self, vault: &str, prefix: &str) -> PluginResult<Vec<String>> {
78 let prefix = if prefix.is_empty() {
82 String::new()
83 } else {
84 validate_key(prefix)?
85 };
86 self.store.list(vault, &prefix).await
87 }
88}
89
90pub(crate) fn validate_key(key: &str) -> PluginResult<String> {
96 if key.is_empty() {
97 return Err(PluginError::invalid_input("storage key must not be empty"));
98 }
99 if key.len() > MAX_KEY_LEN {
100 return Err(PluginError::invalid_input(format!(
101 "storage key is {} bytes; the maximum is {MAX_KEY_LEN}",
102 key.len()
103 )));
104 }
105 if key.starts_with('/') || key.starts_with('\\') {
106 return Err(PluginError::invalid_input(format!(
107 "storage key {key:?} must be relative"
108 )));
109 }
110 for segment in key.split('/') {
111 if segment.is_empty() || segment == "." || segment == ".." {
112 return Err(PluginError::invalid_input(format!(
113 "storage key {key:?} must not contain empty or relative segments"
114 )));
115 }
116 if segment
117 .chars()
118 .any(|character| character == '\\' || character == '\0' || character.is_control())
119 {
120 return Err(PluginError::invalid_input(format!(
121 "storage key {key:?} contains an unsupported character"
122 )));
123 }
124 }
125 Ok(key.to_string())
126}
127
128const MAX_KEY_LEN: usize = 200;
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn keys_may_group_with_slashes() {
138 for valid in ["index", "embeddings/v2/shard-0.bin", "watermark.json"] {
139 assert_eq!(validate_key(valid).expect("valid key"), valid);
140 }
141 }
142
143 #[test]
144 fn keys_cannot_escape_the_namespace() {
145 for rejected in [
146 "",
147 "/absolute",
148 "..",
149 "../sibling/data",
150 "nested/../../escape",
151 "trailing/",
152 "back\\slash",
153 ] {
154 let error = validate_key(rejected).expect_err("must reject");
155 assert_eq!(error.code, crate::PluginErrorCode::InvalidInput);
156 }
157 assert!(validate_key(&"k".repeat(MAX_KEY_LEN + 1)).is_err());
158 }
159}