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