Skip to main content

sim_table_db/
db_dir.rs

1//! The [`DbDir`] object and its library registration: a path-addressed,
2//! capability-gated directory tree of symbol-keyed values implementing the
3//! kernel table and directory contracts, plus the [`install_db_dir_lib`] entry
4//! point.
5
6use std::{
7    collections::{BTreeMap, BTreeSet},
8    sync::{Arc, Mutex},
9};
10
11use sim_kernel::{
12    Cx, Error, Expr, Object, ObjectEncode, ObjectEncoding, Result, Symbol, Value,
13    capability::{
14        table_db_capability, table_db_mkdir_capability, table_db_read_capability,
15        table_db_rmdir_capability, table_db_write_capability,
16    },
17    id::CORE_TABLE_CLASS_ID,
18    object::ClassRef,
19    table::{Dir, Table},
20};
21
22use crate::citizen::db_dir_class_symbol;
23
24struct Store {
25    values: BTreeMap<(String, Symbol), Value>,
26    dirs: BTreeSet<String>,
27}
28
29/// A node in a path-addressed, capability-gated directory tree of symbol-keyed
30/// values.
31///
32/// This is an in-memory table/directory backend, not an external database
33/// engine: every `DbDir` is a view onto a shared store (a `BTreeMap` of values
34/// and a `BTreeSet` of directory paths behind a `Mutex`) rooted at a particular
35/// path. As a [`Table`] it reads and writes the values at its own path; as a
36/// [`Dir`] it creates, opens, and removes child directories, each of which is
37/// another `DbDir` sharing the same store. Cloning a `DbDir` shares the
38/// underlying store. Every operation requires the relevant `table/db`
39/// capability (read, write, mkdir, or rmdir), and child names must be legal
40/// single path segments.
41#[derive(Clone)]
42pub struct DbDir {
43    store: Arc<Mutex<Store>>,
44    path: String,
45}
46
47impl DbDir {
48    /// Open a fresh store and return a `DbDir` rooted at its top-level
49    /// directory.
50    pub fn open() -> Self {
51        let mut dirs = BTreeSet::new();
52        dirs.insert(String::new());
53        Self {
54            store: Arc::new(Mutex::new(Store {
55                values: BTreeMap::new(),
56                dirs,
57            })),
58            path: String::new(),
59        }
60    }
61
62    fn with_store(store: Arc<Mutex<Store>>, path: String) -> Self {
63        Self { store, path }
64    }
65
66    fn lock(&self) -> Result<std::sync::MutexGuard<'_, Store>> {
67        self.store
68            .lock()
69            .map_err(|_| Error::Eval("table/db lock poisoned".into()))
70    }
71
72    fn child_path(&self, name: &Symbol) -> Result<String> {
73        let segment = name.name.as_ref();
74        if !sim_table_core::is_legal_table_segment(segment) {
75            return Err(Error::Eval(format!("table/db: illegal name {segment:?}")));
76        }
77        Ok(if self.path.is_empty() {
78            segment.to_owned()
79        } else {
80            format!("{}/{segment}", self.path)
81        })
82    }
83
84    fn direct_subdirs(&self, store: &Store) -> Vec<Symbol> {
85        let prefix = if self.path.is_empty() {
86            None
87        } else {
88            Some(format!("{}/", self.path))
89        };
90        let mut names = BTreeSet::new();
91        for path in &store.dirs {
92            if path.is_empty() || *path == self.path {
93                continue;
94            }
95            let Some(rest) = prefix
96                .as_ref()
97                .map_or_else(|| Some(path.as_str()), |prefix| path.strip_prefix(prefix))
98            else {
99                continue;
100            };
101            if let Some((head, tail)) = rest.split_once('/') {
102                if !head.is_empty() && !tail.is_empty() {
103                    names.insert(Symbol::new(head));
104                }
105            } else if !rest.is_empty() {
106                names.insert(Symbol::new(rest));
107            }
108        }
109        names.into_iter().collect()
110    }
111
112    fn descriptor_path(&self) -> Vec<String> {
113        if self.path.is_empty() {
114            Vec::new()
115        } else {
116            self.path
117                .split('/')
118                .map(std::borrow::ToOwned::to_owned)
119                .collect()
120        }
121    }
122}
123
124impl Default for DbDir {
125    fn default() -> Self {
126        Self::open()
127    }
128}
129
130impl Object for DbDir {
131    fn display(&self, _cx: &mut Cx) -> Result<String> {
132        if self.path.is_empty() {
133            Ok("table/db[/]".to_owned())
134        } else {
135            Ok(format!("table/db[/{}]", self.path))
136        }
137    }
138
139    fn as_any(&self) -> &dyn std::any::Any {
140        self
141    }
142}
143
144impl sim_kernel::ObjectCompat for DbDir {
145    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
146        let symbol = db_dir_class_symbol();
147        if let Some(value) = cx.registry().class_by_symbol(&symbol) {
148            return Ok(value.clone());
149        }
150        let symbol = Symbol::qualified("core", "Table");
151        if let Some(value) = cx.registry().class_by_symbol(&symbol) {
152            return Ok(value.clone());
153        }
154        cx.factory().class_stub(CORE_TABLE_CLASS_ID, symbol)
155    }
156    fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
157        self.as_table_expr(cx)
158    }
159    fn truth(&self, cx: &mut Cx) -> Result<bool> {
160        Ok(!self.is_empty(cx)?)
161    }
162    fn as_table_impl(&self) -> Option<&dyn Table> {
163        Some(self)
164    }
165    fn as_dir(&self) -> Option<&dyn Dir> {
166        Some(self)
167    }
168    fn as_object_encoder(&self) -> Option<&dyn ObjectEncode> {
169        Some(self)
170    }
171}
172
173impl ObjectEncode for DbDir {
174    fn object_encoding(&self, _cx: &mut Cx) -> Result<ObjectEncoding> {
175        Ok(ObjectEncoding::Constructor {
176            class: db_dir_class_symbol(),
177            args: vec![
178                Expr::Symbol(Symbol::new("v0")),
179                sim_table_core::citizen_fields::path_segments::encode(&self.descriptor_path()),
180            ],
181        })
182    }
183}
184
185impl sim_citizen::Citizen for DbDir {
186    fn citizen_symbol() -> Symbol {
187        db_dir_class_symbol()
188    }
189
190    fn citizen_version() -> u32 {
191        0
192    }
193
194    fn citizen_arity() -> usize {
195        1
196    }
197
198    fn citizen_fields() -> &'static [&'static str] {
199        &["path"]
200    }
201}
202
203impl Table for DbDir {
204    fn backend_symbol(&self) -> Symbol {
205        Symbol::qualified("table", "db")
206    }
207
208    fn get(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
209        cx.require(&table_db_read_capability())?;
210        let value = self.lock()?.values.get(&(self.path.clone(), key)).cloned();
211        match value {
212            Some(value) => Ok(value),
213            None => cx.factory().nil(),
214        }
215    }
216
217    fn set(&self, cx: &mut Cx, key: Symbol, value: Value) -> Result<()> {
218        cx.require(&table_db_write_capability())?;
219        let path = self.child_path(&key)?;
220        let mut store = self.lock()?;
221        if store.dirs.contains(&path) {
222            return Err(Error::Eval(format!("table/db: {key} is a directory")));
223        }
224        store.values.insert((self.path.clone(), key), value);
225        Ok(())
226    }
227
228    fn has(&self, cx: &mut Cx, key: Symbol) -> Result<bool> {
229        cx.require(&table_db_read_capability())?;
230        let path = self.child_path(&key)?;
231        let store = self.lock()?;
232        Ok(store.values.contains_key(&(self.path.clone(), key)) || store.dirs.contains(&path))
233    }
234
235    fn del(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
236        cx.require(&table_db_write_capability())?;
237        let value = self.lock()?.values.remove(&(self.path.clone(), key));
238        match value {
239            Some(value) => Ok(value),
240            None => cx.factory().nil(),
241        }
242    }
243
244    fn keys(&self, cx: &mut Cx) -> Result<Vec<Symbol>> {
245        cx.require(&table_db_read_capability())?;
246        let store = self.lock()?;
247        let mut keys = BTreeSet::new();
248        for (path, key) in store.values.keys() {
249            if *path == self.path {
250                keys.insert(key.clone());
251            }
252        }
253        for key in self.direct_subdirs(&store) {
254            keys.insert(key);
255        }
256        Ok(keys.into_iter().collect())
257    }
258
259    fn entries(&self, cx: &mut Cx) -> Result<Vec<(Symbol, Value)>> {
260        cx.require(&table_db_read_capability())?;
261        let store = self.lock()?;
262        Ok(store
263            .values
264            .iter()
265            .filter(|((path, _), _)| *path == self.path)
266            .map(|((_, key), value)| (key.clone(), value.clone()))
267            .collect())
268    }
269
270    fn len(&self, cx: &mut Cx) -> Result<usize> {
271        Ok(self.entries(cx)?.len())
272    }
273
274    fn clear(&self, cx: &mut Cx) -> Result<()> {
275        cx.require(&table_db_write_capability())?;
276        self.lock()?
277            .values
278            .retain(|(path, _), _| *path != self.path);
279        Ok(())
280    }
281}
282
283impl Dir for DbDir {
284    fn mkdir(&self, cx: &mut Cx, name: Symbol) -> Result<Value> {
285        cx.require(&table_db_mkdir_capability())?;
286        let path = self.child_path(&name)?;
287        let mut store = self.lock()?;
288        if store
289            .values
290            .contains_key(&(self.path.clone(), name.clone()))
291        {
292            return Err(Error::Eval(format!("table/db: {name} is a file")));
293        }
294        store.dirs.insert(path.clone());
295        cx.factory()
296            .opaque(Arc::new(Self::with_store(self.store.clone(), path)))
297    }
298
299    fn opendir(&self, cx: &mut Cx, name: Symbol) -> Result<Option<Value>> {
300        cx.require(&table_db_read_capability())?;
301        let path = self.child_path(&name)?;
302        let store = self.lock()?;
303        if store.dirs.contains(&path) {
304            return Ok(Some(
305                cx.factory()
306                    .opaque(Arc::new(Self::with_store(self.store.clone(), path)))?,
307            ));
308        }
309        if store
310            .values
311            .contains_key(&(self.path.clone(), name.clone()))
312        {
313            return Err(Error::Eval(format!("table/db: {name} is not a directory")));
314        }
315        Ok(None)
316    }
317
318    fn rmdir(&self, cx: &mut Cx, name: Symbol) -> Result<Value> {
319        cx.require(&table_db_rmdir_capability())?;
320        let path = self.child_path(&name)?;
321        let mut store = self.lock()?;
322        if !store.dirs.contains(&path) {
323            return Err(Error::Eval(format!("table/db: {name} is not a directory")));
324        }
325        let prefix = format!("{path}/");
326        store
327            .values
328            .retain(|(entry_path, _), _| *entry_path != path && !entry_path.starts_with(&prefix));
329        store
330            .dirs
331            .retain(|dir_path| *dir_path != path && !dir_path.starts_with(&prefix));
332        cx.factory().nil()
333    }
334
335    fn is_dir(&self, cx: &mut Cx, name: Symbol) -> Result<bool> {
336        cx.require(&table_db_read_capability())?;
337        let path = self.child_path(&name)?;
338        Ok(self.lock()?.dirs.contains(&path))
339    }
340}
341
342/// Open a fresh db-directory store and return its root [`DbDir`] wrapped as an
343/// opaque table/directory object.
344///
345/// # Errors
346///
347/// Returns a capability error if the `table/db` capability has not been granted
348/// to `cx`. Note that the individual read, write, mkdir, and rmdir operations
349/// on the returned directory are gated by their own capabilities.
350///
351/// # Examples
352///
353/// ```
354/// use std::sync::Arc;
355/// use sim_kernel::{
356///     Cx, DefaultFactory, EagerPolicy, Symbol, Table,
357///     capability::{table_db_capability, table_db_read_capability, table_db_write_capability},
358/// };
359/// use sim_table_db::install_db_dir_lib;
360///
361/// let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
362/// cx.grant(table_db_capability());
363/// cx.grant(table_db_read_capability());
364/// cx.grant(table_db_write_capability());
365///
366/// let root = install_db_dir_lib(&mut cx).unwrap();
367/// let table = root.object().as_table_impl().unwrap();
368/// let value = cx.factory().string("v".to_owned()).unwrap();
369/// table.set(&mut cx, Symbol::new("k"), value.clone()).unwrap();
370/// assert_eq!(table.get(&mut cx, Symbol::new("k")).unwrap(), value);
371/// ```
372pub fn install_db_dir_lib(cx: &mut Cx) -> Result<Value> {
373    cx.require(&table_db_capability())?;
374    cx.factory().opaque(Arc::new(DbDir::open()))
375}