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#[derive(Clone)]
23pub struct FsDir {
24 root: PathBuf,
25}
26
27impl FsDir {
28 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
271pub 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}