Skip to main content

sim_table_fs/
fs_dir.rs

1use std::{collections::BTreeSet, path::PathBuf, sync::Arc};
2
3use sim_kernel::{
4    Cx, Error, Expr, Object, ObjectEncode, ObjectEncoding, Result, Symbol, Value,
5    id::CORE_TABLE_CLASS_ID,
6    object::ClassRef,
7    table::{Dir, Table},
8};
9
10use crate::{
11    capabilities::{require_table_fs_read, require_table_fs_write},
12    citizen::fs_dir_class_symbol,
13    roadmap11::{infer_ext_from_expr, known_exts},
14    table_fs_capability,
15};
16
17mod leaf_io;
18
19const DEFAULT_EXT: &str = "siml";
20
21/// A SIM table backed by a host directory rooted at a canonical path.
22#[derive(Clone)]
23pub struct FsDir {
24    root: PathBuf,
25}
26
27impl FsDir {
28    /// Opens (creating if needed) the directory at `root` as a filesystem table.
29    ///
30    /// The root is created if it does not exist and then canonicalized; an I/O
31    /// failure on either step is reported as an error.
32    pub fn open(root: PathBuf) -> Result<Self> {
33        std::fs::create_dir_all(&root)
34            .map_err(|err| Error::Eval(format!("table/fs: cannot open root: {err}")))?;
35        let root = std::fs::canonicalize(&root)
36            .map_err(|err| Error::Eval(format!("table/fs: cannot open root: {err}")))?;
37        Ok(Self { root })
38    }
39}
40
41impl Object for FsDir {
42    fn display(&self, _cx: &mut Cx) -> Result<String> {
43        Ok(format!("table/fs[{}]", self.root.display()))
44    }
45
46    fn as_any(&self) -> &dyn std::any::Any {
47        self
48    }
49}
50
51impl sim_kernel::ObjectCompat for FsDir {
52    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
53        let symbol = fs_dir_class_symbol();
54        if let Some(value) = cx.registry().class_by_symbol(&symbol) {
55            return Ok(value.clone());
56        }
57        let symbol = Symbol::qualified("core", "Table");
58        if let Some(value) = cx.registry().class_by_symbol(&symbol) {
59            return Ok(value.clone());
60        }
61        cx.factory().class_stub(CORE_TABLE_CLASS_ID, symbol)
62    }
63    fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
64        self.as_table_expr(cx)
65    }
66    fn truth(&self, cx: &mut Cx) -> Result<bool> {
67        Ok(!self.is_empty(cx)?)
68    }
69    fn as_table_impl(&self) -> Option<&dyn Table> {
70        Some(self)
71    }
72    fn as_dir(&self) -> Option<&dyn Dir> {
73        Some(self)
74    }
75    fn as_object_encoder(&self) -> Option<&dyn ObjectEncode> {
76        Some(self)
77    }
78}
79
80impl ObjectEncode for FsDir {
81    fn object_encoding(&self, _cx: &mut Cx) -> Result<ObjectEncoding> {
82        Ok(ObjectEncoding::Constructor {
83            class: fs_dir_class_symbol(),
84            args: vec![
85                Expr::Symbol(Symbol::new("v0")),
86                Expr::String(self.root.display().to_string()),
87            ],
88        })
89    }
90}
91
92impl sim_citizen::Citizen for FsDir {
93    fn citizen_symbol() -> Symbol {
94        fs_dir_class_symbol()
95    }
96
97    fn citizen_version() -> u32 {
98        0
99    }
100
101    fn citizen_arity() -> usize {
102        1
103    }
104
105    fn citizen_fields() -> &'static [&'static str] {
106        &["root"]
107    }
108}
109
110impl Table for FsDir {
111    fn backend_symbol(&self) -> Symbol {
112        Symbol::qualified("table", "fs")
113    }
114
115    fn get(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
116        require_table_fs_read(cx)?;
117        match self.leaf_path_for_read(&key)? {
118            Some(_) => {
119                let (_, _, expr) = self.read_leaf_expr(cx, &key)?;
120                cx.factory().expr(expr)
121            }
122            None => cx.factory().nil(),
123        }
124    }
125
126    fn set(&self, cx: &mut Cx, key: Symbol, value: Value) -> Result<()> {
127        require_table_fs_write(cx)?;
128        let base = self.segment(&key)?;
129        if base.is_dir() {
130            return Err(Error::Eval(format!("table/fs: {key} is a directory")));
131        }
132        let existing_leaf = self.leaf_path_for_read(&key)?;
133        for (path, _) in self.leaf_candidates(&key)? {
134            if Some(path.clone()) != existing_leaf.as_ref().map(|(path, _)| path.clone())
135                && path.extension().and_then(|ext| ext.to_str()) != Some(DEFAULT_EXT)
136            {
137                std::fs::remove_file(&path)
138                    .map_err(|err| Error::Eval(format!("table/fs: write {err}")))?;
139            }
140        }
141        let expr = value.object().as_expr(cx)?;
142        let ext = existing_leaf
143            .as_ref()
144            .map(|(_, ext)| *ext)
145            .or_else(|| infer_ext_from_expr(&expr))
146            .unwrap_or(DEFAULT_EXT);
147        let path = base.with_extension(ext);
148        self.ensure_within_root(&path)?;
149        let bytes = Self::encode_leaf_expr(cx, ext, &expr)?;
150        std::fs::write(&path, bytes)
151            .map_err(|err| Error::Eval(format!("table/fs: write {err}")))?;
152        Ok(())
153    }
154
155    fn has(&self, cx: &mut Cx, key: Symbol) -> Result<bool> {
156        require_table_fs_read(cx)?;
157        let path = self.segment(&key)?;
158        Ok(path.is_dir() || self.leaf_path_for_read(&key)?.is_some())
159    }
160
161    fn del(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
162        require_table_fs_read(cx)?;
163        require_table_fs_write(cx)?;
164        match self.leaf_path_for_read(&key)? {
165            Some((path, ext)) => {
166                let expr = self.read_leaf_path(cx, &path, ext)?;
167                std::fs::remove_file(&path)
168                    .map_err(|err| Error::Eval(format!("table/fs: del {err}")))?;
169                cx.factory().expr(expr)
170            }
171            None => cx.factory().nil(),
172        }
173    }
174
175    fn keys(&self, cx: &mut Cx) -> Result<Vec<Symbol>> {
176        require_table_fs_read(cx)?;
177        let mut keys = BTreeSet::new();
178        let entries = std::fs::read_dir(&self.root)
179            .map_err(|err| Error::Eval(format!("table/fs: read_dir {err}")))?;
180        for entry in entries {
181            let entry = entry.map_err(|err| Error::Eval(format!("table/fs: {err}")))?;
182            let path = entry.path();
183            self.ensure_within_root(&path)?;
184            let name = entry.file_name().to_string_lossy().to_string();
185            if name.starts_with('.') {
186                continue;
187            }
188            if path.is_dir() {
189                keys.insert(Symbol::new(name));
190                continue;
191            }
192            let Some(stem) = known_exts().into_iter().find_map(|ext| {
193                name.strip_suffix(&format!(".{ext}"))
194                    .map(std::borrow::ToOwned::to_owned)
195            }) else {
196                continue;
197            };
198            keys.insert(Symbol::new(stem));
199        }
200        Ok(keys.into_iter().collect())
201    }
202
203    fn entries(&self, cx: &mut Cx) -> Result<Vec<(Symbol, Value)>> {
204        require_table_fs_read(cx)?;
205        let mut entries = Vec::new();
206        for key in self.keys(cx)? {
207            if self.is_dir(cx, key.clone())? {
208                continue;
209            }
210            entries.push((key.clone(), self.get(cx, key)?));
211        }
212        Ok(entries)
213    }
214
215    fn len(&self, cx: &mut Cx) -> Result<usize> {
216        Ok(self.entries(cx)?.len())
217    }
218
219    fn clear(&self, cx: &mut Cx) -> Result<()> {
220        require_table_fs_write(cx)?;
221        for key in self.keys(cx)? {
222            if !self.is_dir(cx, key.clone())? {
223                let _ = self.del(cx, key)?;
224            }
225        }
226        Ok(())
227    }
228}
229
230impl Dir for FsDir {
231    fn mkdir(&self, cx: &mut Cx, name: Symbol) -> Result<Value> {
232        require_table_fs_write(cx)?;
233        let path = self.segment(&name)?;
234        if self.leaf_path_for_read(&name)?.is_some() {
235            return Err(Error::Eval(format!("table/fs: {name} is a file")));
236        }
237        std::fs::create_dir_all(&path)
238            .map_err(|err| Error::Eval(format!("table/fs: mkdir {err}")))?;
239        cx.factory().opaque(Arc::new(Self::open(path)?))
240    }
241
242    fn opendir(&self, cx: &mut Cx, name: Symbol) -> Result<Option<Value>> {
243        require_table_fs_read(cx)?;
244        let path = self.segment(&name)?;
245        if path.is_dir() {
246            Ok(Some(cx.factory().opaque(Arc::new(Self::open(path)?))?))
247        } else if path.exists() || self.leaf_path_for_read(&name)?.is_some() {
248            Err(Error::Eval(format!("table/fs: {name} is not a directory")))
249        } else {
250            Ok(None)
251        }
252    }
253
254    fn rmdir(&self, cx: &mut Cx, name: Symbol) -> Result<Value> {
255        require_table_fs_write(cx)?;
256        let path = self.segment(&name)?;
257        if !path.is_dir() {
258            return Err(Error::Eval(format!("table/fs: {name} is not a directory")));
259        }
260        std::fs::remove_dir_all(&path)
261            .map_err(|err| Error::Eval(format!("table/fs: rmdir {err}")))?;
262        cx.factory().nil()
263    }
264
265    fn is_dir(&self, cx: &mut Cx, name: Symbol) -> Result<bool> {
266        require_table_fs_read(cx)?;
267        Ok(self.segment(&name)?.is_dir())
268    }
269}
270
271/// Opens a filesystem table at `root` and returns it as a runtime table value.
272///
273/// Requires the table-fs capability; the returned value wraps an [`FsDir`].
274pub fn install_fs_dir_lib(cx: &mut Cx, root: &str) -> Result<Value> {
275    cx.require(&table_fs_capability())?;
276    let dir = FsDir::open(PathBuf::from(root))?;
277    cx.factory().opaque(Arc::new(dir))
278}