Skip to main content

sim_table_mount/
mount_dir.rs

1//! Mounted directory object and routing implementation.
2
3use std::{
4    collections::{BTreeMap, BTreeSet},
5    sync::{Arc, Mutex},
6};
7
8use sim_kernel::{
9    Cx, Error, Expr, Object, Result, Symbol, Value,
10    id::CORE_TABLE_CLASS_ID,
11    object::ClassRef,
12    table::{Dir, Table},
13};
14use sim_table_core::TablePath;
15
16use crate::{
17    routing::{
18        ResolvedNode, check_segment, format_path, has_mount_descendant, is_prefix, join_child,
19        longest_mount, owned_path, require_dir_value, require_table_value, resolve_mounted_target,
20        traverse_dir_path,
21    },
22    table_mount_capability,
23};
24
25pub(crate) type MountPath = Vec<String>;
26
27#[derive(Clone)]
28pub(crate) struct MountTarget {
29    pub(crate) kind: MountKind,
30    pub(crate) value: Value,
31}
32
33/// The mounted target kind at a mount point.
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum MountKind {
36    /// A Table leaf mounted at one exact path.
37    Table,
38    /// A Dir mounted at one path and available below that path.
39    Dir,
40}
41
42impl MountKind {
43    pub(crate) fn label(self) -> &'static str {
44        match self {
45            Self::Table => "table",
46            Self::Dir => "dir",
47        }
48    }
49}
50
51/// One row returned by [`MountedDir::inspect`].
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct MountInspection {
54    /// Absolute mount point.
55    pub path: String,
56    /// Mounted target kind.
57    pub kind: MountKind,
58}
59
60/// A live Table/Dir namespace assembled from a root Dir and explicit mounts.
61#[derive(Clone)]
62pub struct MountedDir {
63    root: Value,
64    mounts: Arc<Mutex<BTreeMap<MountPath, MountTarget>>>,
65    path: MountPath,
66}
67
68impl MountedDir {
69    /// Create a mounted namespace rooted at `root`.
70    ///
71    /// `root` must be a directory value. The namespace starts with no mounts.
72    pub fn new(root: Value) -> Result<Self> {
73        require_dir_value(&root, "table/mount: root must be a Dir")?;
74        Ok(Self {
75            root,
76            mounts: Arc::new(Mutex::new(BTreeMap::new())),
77            path: Vec::new(),
78        })
79    }
80
81    /// Mount `target` as a directory at absolute `path`.
82    pub fn mount_dir(&self, cx: &mut Cx, path: TablePath, target: Value) -> Result<()> {
83        cx.require(&table_mount_capability())?;
84        require_dir_value(&target, "table/mount: Dir mount target must be a Dir")?;
85        self.insert_mount(cx, path, MountKind::Dir, target)
86    }
87
88    /// Mount `target` as a table leaf at absolute `path`.
89    pub fn mount_table(&self, cx: &mut Cx, path: TablePath, target: Value) -> Result<()> {
90        cx.require(&table_mount_capability())?;
91        require_table_value(&target, "table/mount: table mount target must be a Table")?;
92        self.insert_mount(cx, path, MountKind::Table, target)
93    }
94
95    /// Remove the exact mount at absolute `path`.
96    pub fn unmount(&self, cx: &mut Cx, path: &TablePath) -> Result<Option<Value>> {
97        cx.require(&table_mount_capability())?;
98        let path = owned_path(path);
99        if path.is_empty() {
100            return Err(Error::Eval("table/mount: cannot unmount root".to_owned()));
101        }
102        Ok(self.lock_mounts()?.remove(&path).map(|target| target.value))
103    }
104
105    /// Return the current mount table as absolute paths and target kinds.
106    pub fn inspect(&self) -> Result<Vec<MountInspection>> {
107        Ok(self
108            .lock_mounts()?
109            .iter()
110            .map(|(path, target)| MountInspection {
111                path: format_path(path),
112                kind: target.kind,
113            })
114            .collect())
115    }
116
117    fn at_path(&self, path: MountPath) -> Self {
118        Self {
119            root: self.root.clone(),
120            mounts: self.mounts.clone(),
121            path,
122        }
123    }
124
125    fn insert_mount(
126        &self,
127        cx: &mut Cx,
128        path: TablePath,
129        kind: MountKind,
130        target: Value,
131    ) -> Result<()> {
132        let path = owned_path(&path);
133        if path.is_empty() {
134            return Err(Error::Eval("table/mount: cannot mount root".to_owned()));
135        }
136        if self.root_exact_kind(cx, &path)?.is_some() {
137            return Err(Error::Eval(format!(
138                "table/mount: mount point {} shadows an existing root node",
139                format_path(&path)
140            )));
141        }
142
143        let mut mounts = self.lock_mounts()?;
144        if mounts.contains_key(&path) {
145            return Err(Error::Eval(format!(
146                "table/mount: duplicate mount point {}",
147                format_path(&path)
148            )));
149        }
150        for (existing_path, existing) in mounts.iter() {
151            if is_prefix(existing_path, &path)
152                && existing.kind == MountKind::Table
153                && existing_path.len() < path.len()
154            {
155                return Err(Error::Eval(format!(
156                    "table/mount: cannot mount below table leaf {}",
157                    format_path(existing_path)
158                )));
159            }
160            if is_prefix(&path, existing_path) && kind == MountKind::Table {
161                return Err(Error::Eval(format!(
162                    "table/mount: table mount {} would parent existing mount {}",
163                    format_path(&path),
164                    format_path(existing_path)
165                )));
166            }
167        }
168        mounts.insert(
169            path,
170            MountTarget {
171                kind,
172                value: target,
173            },
174        );
175        Ok(())
176    }
177
178    fn lock_mounts(&self) -> Result<std::sync::MutexGuard<'_, BTreeMap<MountPath, MountTarget>>> {
179        self.mounts
180            .lock()
181            .map_err(|_| Error::Eval("table/mount: mount registry lock poisoned".to_owned()))
182    }
183
184    fn mount_snapshot(&self) -> Result<BTreeMap<MountPath, MountTarget>> {
185        Ok(self.lock_mounts()?.clone())
186    }
187
188    fn resolve_current(&self, cx: &mut Cx) -> Result<ResolvedNode> {
189        let mounts = self.mount_snapshot()?;
190        if let Some((prefix_len, target)) = longest_mount(&mounts, &self.path) {
191            return resolve_mounted_target(cx, target, &self.path[prefix_len..]);
192        }
193        match traverse_dir_path(cx, self.root.clone(), &self.path)? {
194            Some(value) => Ok(ResolvedNode::Backed(value)),
195            None if has_mount_descendant(&mounts, &self.path) => Ok(ResolvedNode::Virtual),
196            None => Err(Error::Eval(format!(
197                "table/mount: {} has no backing directory",
198                format_path(&self.path)
199            ))),
200        }
201    }
202
203    fn root_exact_kind(&self, cx: &mut Cx, path: &[String]) -> Result<Option<MountKind>> {
204        if path.is_empty() {
205            return Ok(Some(MountKind::Dir));
206        }
207        let parent_path = &path[..path.len() - 1];
208        let Some(parent) = traverse_dir_path(cx, self.root.clone(), parent_path)? else {
209            return Ok(None);
210        };
211        let dir = require_dir_value(&parent, "table/mount: root path parent is not a Dir")?;
212        let table = require_table_value(&parent, "table/mount: root path parent is not a Table")?;
213        let name = Symbol::new(path.last().expect("non-empty path").as_str());
214        if dir.is_dir(cx, name.clone())? {
215            return Ok(Some(MountKind::Dir));
216        }
217        if table.has(cx, name)? {
218            return Ok(Some(MountKind::Table));
219        }
220        Ok(None)
221    }
222
223    fn direct_mount_children(&self) -> Result<BTreeMap<Symbol, MountKind>> {
224        let mut children = BTreeMap::new();
225        for (path, target) in self.lock_mounts()?.iter() {
226            if path.len() <= self.path.len() || !is_prefix(&self.path, path) {
227                continue;
228            }
229            let name = Symbol::new(path[self.path.len()].as_str());
230            let kind = if path.len() == self.path.len() + 1 {
231                target.kind
232            } else {
233                MountKind::Dir
234            };
235            children.entry(name).or_insert(kind);
236        }
237        Ok(children)
238    }
239
240    fn child_has_mount(&self, name: &Symbol) -> Result<bool> {
241        let mut child = self.path.clone();
242        child.push(name.name.to_string());
243        Ok(self
244            .lock_mounts()?
245            .keys()
246            .any(|path| is_prefix(&child, path)))
247    }
248
249    fn direct_mount_value(&self, name: &Symbol) -> Result<Option<MountTarget>> {
250        let mut child = self.path.clone();
251        child.push(name.name.to_string());
252        Ok(self.lock_mounts()?.get(&child).cloned())
253    }
254
255    fn check_no_mount_child(&self, name: &Symbol, action: &str) -> Result<()> {
256        if self.child_has_mount(name)? {
257            return Err(Error::Eval(format!(
258                "table/mount: cannot {action} {} while a mount exists there",
259                join_child(&self.path, name)
260            )));
261        }
262        Ok(())
263    }
264
265    fn backed_table(node: ResolvedNode) -> Result<Option<Value>> {
266        match node {
267            ResolvedNode::Backed(value) => {
268                require_table_value(&value, "table/mount: backing node is not a Table")?;
269                Ok(Some(value))
270            }
271            ResolvedNode::Virtual => Ok(None),
272        }
273    }
274}
275
276impl Object for MountedDir {
277    fn display(&self, _cx: &mut Cx) -> Result<String> {
278        Ok(format!(
279            "table/mount[{}; mounts={}]",
280            format_path(&self.path),
281            self.lock_mounts()?.len()
282        ))
283    }
284
285    fn as_any(&self) -> &dyn std::any::Any {
286        self
287    }
288}
289
290impl sim_kernel::ObjectCompat for MountedDir {
291    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
292        let symbol = Symbol::qualified("core", "Table");
293        if let Some(value) = cx.registry().class_by_symbol(&symbol) {
294            return Ok(value.clone());
295        }
296        cx.factory().class_stub(CORE_TABLE_CLASS_ID, symbol)
297    }
298
299    fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
300        self.as_table_expr(cx)
301    }
302
303    fn truth(&self, cx: &mut Cx) -> Result<bool> {
304        Ok(!self.is_empty(cx)?)
305    }
306
307    fn as_table_impl(&self) -> Option<&dyn Table> {
308        Some(self)
309    }
310
311    fn as_dir(&self) -> Option<&dyn Dir> {
312        Some(self)
313    }
314}
315
316impl Table for MountedDir {
317    fn backend_symbol(&self) -> Symbol {
318        Symbol::qualified("table", "mount")
319    }
320
321    fn get(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
322        if let Some(target) = self.direct_mount_value(&key)? {
323            return match target.kind {
324                MountKind::Table => Ok(target.value),
325                MountKind::Dir => cx.factory().nil(),
326            };
327        }
328        let node = self.resolve_current(cx)?;
329        let Some(value) = Self::backed_table(node)? else {
330            return cx.factory().nil();
331        };
332        require_table_value(&value, "table/mount: backing node is not a Table")?.get(cx, key)
333    }
334
335    fn set(&self, cx: &mut Cx, key: Symbol, value: Value) -> Result<()> {
336        self.check_no_mount_child(&key, "set")?;
337        let node = self.resolve_current(cx)?;
338        let Some(backing) = Self::backed_table(node)? else {
339            return Err(Error::Eval(format!(
340                "table/mount: {} has no writable backing table",
341                format_path(&self.path)
342            )));
343        };
344        require_table_value(&backing, "table/mount: backing node is not a Table")?
345            .set(cx, key, value)
346    }
347
348    fn has(&self, cx: &mut Cx, key: Symbol) -> Result<bool> {
349        if self.child_has_mount(&key)? {
350            return Ok(true);
351        }
352        let node = self.resolve_current(cx)?;
353        let Some(backing) = Self::backed_table(node)? else {
354            return Ok(false);
355        };
356        require_table_value(&backing, "table/mount: backing node is not a Table")?.has(cx, key)
357    }
358
359    fn del(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
360        self.check_no_mount_child(&key, "delete")?;
361        let node = self.resolve_current(cx)?;
362        let Some(backing) = Self::backed_table(node)? else {
363            return cx.factory().nil();
364        };
365        require_table_value(&backing, "table/mount: backing node is not a Table")?.del(cx, key)
366    }
367
368    fn keys(&self, cx: &mut Cx) -> Result<Vec<Symbol>> {
369        let mut keys = BTreeSet::new();
370        let node = self.resolve_current(cx)?;
371        if let Some(backing) = Self::backed_table(node)? {
372            keys.extend(
373                require_table_value(&backing, "table/mount: backing node is not a Table")?
374                    .keys(cx)?,
375            );
376        }
377        keys.extend(self.direct_mount_children()?.into_keys());
378        Ok(keys.into_iter().collect())
379    }
380
381    fn entries(&self, cx: &mut Cx) -> Result<Vec<(Symbol, Value)>> {
382        let mount_children = self.direct_mount_children()?;
383        let mut out = Vec::new();
384        let mut seen = BTreeSet::new();
385        let node = self.resolve_current(cx)?;
386        if let Some(backing) = Self::backed_table(node)? {
387            for (key, value) in
388                require_table_value(&backing, "table/mount: backing node is not a Table")?
389                    .entries(cx)?
390            {
391                if mount_children.contains_key(&key) {
392                    return Err(Error::Eval(format!(
393                        "table/mount: backing entry {key} conflicts with a mount"
394                    )));
395                }
396                seen.insert(key.clone());
397                out.push((key, value));
398            }
399        }
400        for (key, kind) in mount_children {
401            if kind == MountKind::Table && seen.insert(key.clone()) {
402                let target = self
403                    .direct_mount_value(&key)?
404                    .expect("mount child collected from registry");
405                out.push((key, target.value));
406            }
407        }
408        Ok(out)
409    }
410
411    fn len(&self, cx: &mut Cx) -> Result<usize> {
412        Ok(self.entries(cx)?.len())
413    }
414
415    fn clear(&self, cx: &mut Cx) -> Result<()> {
416        if let Some(name) = self.direct_mount_children()?.keys().next().cloned() {
417            return Err(Error::Eval(format!(
418                "table/mount: cannot clear {} while mount child {name} exists",
419                format_path(&self.path)
420            )));
421        }
422        let node = self.resolve_current(cx)?;
423        let Some(backing) = Self::backed_table(node)? else {
424            return Ok(());
425        };
426        require_table_value(&backing, "table/mount: backing node is not a Table")?.clear(cx)
427    }
428}
429
430impl Dir for MountedDir {
431    fn mkdir(&self, cx: &mut Cx, name: Symbol) -> Result<Value> {
432        check_segment(&name)?;
433        self.check_no_mount_child(&name, "mkdir")?;
434        let ResolvedNode::Backed(value) = self.resolve_current(cx)? else {
435            return Err(Error::Eval(format!(
436                "table/mount: {} has no backing directory",
437                format_path(&self.path)
438            )));
439        };
440        require_dir_value(&value, "table/mount: backing node is not a Dir")?.mkdir(cx, name)
441    }
442
443    fn opendir(&self, cx: &mut Cx, name: Symbol) -> Result<Option<Value>> {
444        check_segment(&name)?;
445        let mut child_path = self.path.clone();
446        child_path.push(name.name.to_string());
447        if let Some(target) = self.direct_mount_value(&name)? {
448            return match target.kind {
449                MountKind::Dir => cx
450                    .factory()
451                    .opaque(Arc::new(self.at_path(child_path)))
452                    .map(Some),
453                MountKind::Table => Err(Error::Eval(format!(
454                    "table/mount: {} is a table mount, not a directory",
455                    join_child(&self.path, &name)
456                ))),
457            };
458        }
459        if self.child_has_mount(&name)? {
460            return cx
461                .factory()
462                .opaque(Arc::new(self.at_path(child_path)))
463                .map(Some);
464        }
465        let ResolvedNode::Backed(value) = self.resolve_current(cx)? else {
466            return Ok(None);
467        };
468        let dir = require_dir_value(&value, "table/mount: backing node is not a Dir")?;
469        if dir.opendir(cx, name)?.is_some() {
470            return cx
471                .factory()
472                .opaque(Arc::new(self.at_path(child_path)))
473                .map(Some);
474        }
475        Ok(None)
476    }
477
478    fn rmdir(&self, cx: &mut Cx, name: Symbol) -> Result<Value> {
479        check_segment(&name)?;
480        self.check_no_mount_child(&name, "remove directory")?;
481        let ResolvedNode::Backed(value) = self.resolve_current(cx)? else {
482            return cx.factory().nil();
483        };
484        require_dir_value(&value, "table/mount: backing node is not a Dir")?.rmdir(cx, name)
485    }
486
487    fn is_dir(&self, cx: &mut Cx, name: Symbol) -> Result<bool> {
488        check_segment(&name)?;
489        if let Some(target) = self.direct_mount_value(&name)? {
490            return Ok(target.kind == MountKind::Dir);
491        }
492        if self.child_has_mount(&name)? {
493            return Ok(true);
494        }
495        let ResolvedNode::Backed(value) = self.resolve_current(cx)? else {
496            return Ok(false);
497        };
498        require_dir_value(&value, "table/mount: backing node is not a Dir")?.is_dir(cx, name)
499    }
500}