1use std::{
2 collections::BTreeSet,
3 path::{Path, PathBuf},
4 sync::Arc,
5};
6
7use sim_codec::{Input, Output, decode_with_codec, encode_with_codec};
8use sim_kernel::{
9 Cx, EncodeOptions, Error, Expr, Object, ObjectEncode, ObjectEncoding, ReadPolicy, Result,
10 Symbol, Value,
11 capability::{
12 table_fs_capability, table_fs_mkdir_capability, table_fs_read_capability,
13 table_fs_rmdir_capability, table_fs_write_capability,
14 },
15 id::CORE_TABLE_CLASS_ID,
16 object::ClassRef,
17 table::{Dir, Table},
18};
19
20use crate::citizen::fs_dir_class_symbol;
21use crate::roadmap11::{decode_expr_for_ext, encode_expr_for_ext, infer_ext_from_expr, known_exts};
22
23const DEFAULT_EXT: &str = "siml";
24
25#[derive(Clone)]
27pub struct FsDir {
28 root: PathBuf,
29}
30
31impl FsDir {
32 pub fn open(root: PathBuf) -> Result<Self> {
37 std::fs::create_dir_all(&root)
38 .map_err(|err| Error::Eval(format!("table/fs: cannot open root: {err}")))?;
39 let root = std::fs::canonicalize(&root)
40 .map_err(|err| Error::Eval(format!("table/fs: cannot open root: {err}")))?;
41 Ok(Self { root })
42 }
43
44 fn segment(&self, name: &Symbol) -> Result<PathBuf> {
45 let segment = name.name.as_ref();
46 if !sim_table_core::is_legal_table_segment(segment) || Path::new(segment).is_absolute() {
49 return Err(Error::Eval(format!("table/fs: illegal name {segment:?}")));
50 }
51 let path = self.root.join(segment);
52 self.ensure_within_root(&path)?;
53 Ok(path)
54 }
55
56 fn ensure_within_root(&self, path: &Path) -> Result<()> {
57 let candidate = if path.exists() {
58 std::fs::canonicalize(path)
59 .map_err(|err| Error::Eval(format!("table/fs: path check {err}")))?
60 } else {
61 path.to_path_buf()
62 };
63 if candidate.starts_with(&self.root) {
64 Ok(())
65 } else {
66 Err(Error::Eval(format!(
67 "table/fs: path escapes root: {}",
68 path.display()
69 )))
70 }
71 }
72
73 fn leaf_candidates(&self, name: &Symbol) -> Result<Vec<(PathBuf, &'static str)>> {
74 let base = self.segment(name)?;
75 let mut matches = Vec::new();
76 for ext in known_exts() {
77 let path = base.with_extension(ext);
78 self.ensure_within_root(&path)?;
79 if path.is_file() {
80 matches.push((path, ext));
81 }
82 }
83 Ok(matches)
84 }
85
86 fn leaf_path_for_read(&self, name: &Symbol) -> Result<Option<(PathBuf, &'static str)>> {
87 let matches = self.leaf_candidates(name)?;
88 match matches.len() {
89 0 => Ok(None),
90 1 => Ok(matches.into_iter().next()),
91 _ => Err(Error::Eval(format!(
92 "table/fs: multiple leaf files found for key {name}"
93 ))),
94 }
95 }
96
97 fn codec_for_ext(ext: &str) -> Result<Symbol> {
98 match ext {
99 "siml" => Ok(Symbol::qualified("codec", "lisp")),
100 "simb" => Ok(Symbol::qualified("codec", "binary")),
101 "simb64" => Ok(Symbol::qualified("codec", "binary-base64")),
102 "simj" => Ok(Symbol::qualified("codec", "json")),
103 "sima" => Ok(Symbol::qualified("codec", "algol")),
104 other => Err(Error::Eval(format!("table/fs: unknown extension {other}"))),
105 }
106 }
107
108 fn decode_expr_bytes(cx: &mut Cx, codec: &Symbol, bytes: &[u8]) -> Result<Expr> {
109 decode_with_codec(
110 cx,
111 codec,
112 Input::Bytes(bytes.to_vec()),
113 ReadPolicy::default(),
114 )
115 }
116
117 fn encode_expr_bytes(cx: &mut Cx, codec: &Symbol, expr: &Expr) -> Result<Vec<u8>> {
118 match encode_with_codec(cx, codec, expr, EncodeOptions::default())? {
119 Output::Text(text) => Ok(text.into_bytes()),
120 Output::Bytes(bytes) => Ok(bytes),
121 }
122 }
123}
124
125impl Object for FsDir {
126 fn display(&self, _cx: &mut Cx) -> Result<String> {
127 Ok(format!("table/fs[{}]", self.root.display()))
128 }
129
130 fn as_any(&self) -> &dyn std::any::Any {
131 self
132 }
133}
134
135impl sim_kernel::ObjectCompat for FsDir {
136 fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
137 let symbol = fs_dir_class_symbol();
138 if let Some(value) = cx.registry().class_by_symbol(&symbol) {
139 return Ok(value.clone());
140 }
141 let symbol = Symbol::qualified("core", "Table");
142 if let Some(value) = cx.registry().class_by_symbol(&symbol) {
143 return Ok(value.clone());
144 }
145 cx.factory().class_stub(CORE_TABLE_CLASS_ID, symbol)
146 }
147 fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
148 self.as_table_expr(cx)
149 }
150 fn truth(&self, cx: &mut Cx) -> Result<bool> {
151 Ok(!self.is_empty(cx)?)
152 }
153 fn as_table_impl(&self) -> Option<&dyn Table> {
154 Some(self)
155 }
156 fn as_dir(&self) -> Option<&dyn Dir> {
157 Some(self)
158 }
159 fn as_object_encoder(&self) -> Option<&dyn ObjectEncode> {
160 Some(self)
161 }
162}
163
164impl ObjectEncode for FsDir {
165 fn object_encoding(&self, _cx: &mut Cx) -> Result<ObjectEncoding> {
166 Ok(ObjectEncoding::Constructor {
167 class: fs_dir_class_symbol(),
168 args: vec![
169 Expr::Symbol(Symbol::new("v0")),
170 Expr::String(self.root.display().to_string()),
171 ],
172 })
173 }
174}
175
176impl sim_citizen::Citizen for FsDir {
177 fn citizen_symbol() -> Symbol {
178 fs_dir_class_symbol()
179 }
180
181 fn citizen_version() -> u32 {
182 0
183 }
184
185 fn citizen_arity() -> usize {
186 1
187 }
188
189 fn citizen_fields() -> &'static [&'static str] {
190 &["root"]
191 }
192}
193
194impl Table for FsDir {
195 fn backend_symbol(&self) -> Symbol {
196 Symbol::qualified("table", "fs")
197 }
198
199 fn get(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
200 cx.require(&table_fs_read_capability())?;
201 match self.leaf_path_for_read(&key)? {
202 Some((path, ext)) => {
203 let bytes = std::fs::read(&path)
204 .map_err(|err| Error::Eval(format!("table/fs: read {err}")))?;
205 let expr = match decode_expr_for_ext(ext, &bytes) {
206 Some(expr) => expr?,
207 None => {
208 let codec = Self::codec_for_ext(ext)?;
209 Self::decode_expr_bytes(cx, &codec, &bytes)?
210 }
211 };
212 cx.factory().expr(expr)
213 }
214 None => cx.factory().nil(),
215 }
216 }
217
218 fn set(&self, cx: &mut Cx, key: Symbol, value: Value) -> Result<()> {
219 cx.require(&table_fs_write_capability())?;
220 let base = self.segment(&key)?;
221 if base.is_dir() {
222 return Err(Error::Eval(format!("table/fs: {key} is a directory")));
223 }
224 let existing_leaf = self.leaf_path_for_read(&key)?;
225 for (path, _) in self.leaf_candidates(&key)? {
226 if Some(path.clone()) != existing_leaf.as_ref().map(|(path, _)| path.clone())
227 && path.extension().and_then(|ext| ext.to_str()) != Some(DEFAULT_EXT)
228 {
229 std::fs::remove_file(&path)
230 .map_err(|err| Error::Eval(format!("table/fs: write {err}")))?;
231 }
232 }
233 let expr = value.object().as_expr(cx)?;
234 let ext = existing_leaf
235 .as_ref()
236 .map(|(_, ext)| *ext)
237 .or_else(|| infer_ext_from_expr(&expr))
238 .unwrap_or(DEFAULT_EXT);
239 let path = base.with_extension(ext);
240 self.ensure_within_root(&path)?;
241 let bytes = match encode_expr_for_ext(ext, &expr) {
242 Some(bytes) => bytes?,
243 None => {
244 let codec = Symbol::qualified("codec", "lisp");
245 Self::encode_expr_bytes(cx, &codec, &expr)?
246 }
247 };
248 std::fs::write(&path, bytes)
249 .map_err(|err| Error::Eval(format!("table/fs: write {err}")))?;
250 Ok(())
251 }
252
253 fn has(&self, cx: &mut Cx, key: Symbol) -> Result<bool> {
254 cx.require(&table_fs_read_capability())?;
255 let path = self.segment(&key)?;
256 Ok(path.is_dir() || self.leaf_path_for_read(&key)?.is_some())
257 }
258
259 fn del(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
260 cx.require(&table_fs_write_capability())?;
261 match self.leaf_path_for_read(&key)? {
262 Some((path, ext)) => {
263 let bytes = std::fs::read(&path).unwrap_or_default();
264 std::fs::remove_file(&path)
265 .map_err(|err| Error::Eval(format!("table/fs: del {err}")))?;
266 let expr = match decode_expr_for_ext(ext, &bytes) {
267 Some(expr) => expr,
268 None => {
269 let codec = Self::codec_for_ext(ext)?;
270 Self::decode_expr_bytes(cx, &codec, &bytes)
271 }
272 };
273 match expr {
274 Ok(expr) => cx.factory().expr(expr),
275 Err(_) => cx.factory().nil(),
276 }
277 }
278 None => cx.factory().nil(),
279 }
280 }
281
282 fn keys(&self, cx: &mut Cx) -> Result<Vec<Symbol>> {
283 cx.require(&table_fs_read_capability())?;
284 let mut keys = BTreeSet::new();
285 let entries = std::fs::read_dir(&self.root)
286 .map_err(|err| Error::Eval(format!("table/fs: read_dir {err}")))?;
287 for entry in entries {
288 let entry = entry.map_err(|err| Error::Eval(format!("table/fs: {err}")))?;
289 let path = entry.path();
290 self.ensure_within_root(&path)?;
291 let name = entry.file_name().to_string_lossy().to_string();
292 if name.starts_with('.') {
293 continue;
294 }
295 if path.is_dir() {
296 keys.insert(Symbol::new(name));
297 continue;
298 }
299 let Some(stem) = known_exts().into_iter().find_map(|ext| {
300 name.strip_suffix(&format!(".{ext}"))
301 .map(std::borrow::ToOwned::to_owned)
302 }) else {
303 continue;
304 };
305 keys.insert(Symbol::new(stem));
306 }
307 Ok(keys.into_iter().collect())
308 }
309
310 fn entries(&self, cx: &mut Cx) -> Result<Vec<(Symbol, Value)>> {
311 cx.require(&table_fs_read_capability())?;
312 let mut entries = Vec::new();
313 for key in self.keys(cx)? {
314 if self.is_dir(cx, key.clone())? {
315 continue;
316 }
317 entries.push((key.clone(), self.get(cx, key)?));
318 }
319 Ok(entries)
320 }
321
322 fn len(&self, cx: &mut Cx) -> Result<usize> {
323 Ok(self.entries(cx)?.len())
324 }
325
326 fn clear(&self, cx: &mut Cx) -> Result<()> {
327 cx.require(&table_fs_write_capability())?;
328 for key in self.keys(cx)? {
329 if !self.is_dir(cx, key.clone())? {
330 let _ = self.del(cx, key)?;
331 }
332 }
333 Ok(())
334 }
335}
336
337impl Dir for FsDir {
338 fn mkdir(&self, cx: &mut Cx, name: Symbol) -> Result<Value> {
339 cx.require(&table_fs_mkdir_capability())?;
340 let path = self.segment(&name)?;
341 if self.leaf_path_for_read(&name)?.is_some() {
342 return Err(Error::Eval(format!("table/fs: {name} is a file")));
343 }
344 std::fs::create_dir_all(&path)
345 .map_err(|err| Error::Eval(format!("table/fs: mkdir {err}")))?;
346 cx.factory().opaque(Arc::new(Self::open(path)?))
347 }
348
349 fn opendir(&self, cx: &mut Cx, name: Symbol) -> Result<Option<Value>> {
350 cx.require(&table_fs_read_capability())?;
351 let path = self.segment(&name)?;
352 if path.is_dir() {
353 Ok(Some(cx.factory().opaque(Arc::new(Self::open(path)?))?))
354 } else if path.exists() || self.leaf_path_for_read(&name)?.is_some() {
355 Err(Error::Eval(format!("table/fs: {name} is not a directory")))
356 } else {
357 Ok(None)
358 }
359 }
360
361 fn rmdir(&self, cx: &mut Cx, name: Symbol) -> Result<Value> {
362 cx.require(&table_fs_rmdir_capability())?;
363 let path = self.segment(&name)?;
364 if !path.is_dir() {
365 return Err(Error::Eval(format!("table/fs: {name} is not a directory")));
366 }
367 std::fs::remove_dir_all(&path)
368 .map_err(|err| Error::Eval(format!("table/fs: rmdir {err}")))?;
369 cx.factory().nil()
370 }
371
372 fn is_dir(&self, cx: &mut Cx, name: Symbol) -> Result<bool> {
373 cx.require(&table_fs_read_capability())?;
374 Ok(self.segment(&name)?.is_dir())
375 }
376}
377
378pub fn install_fs_dir_lib(cx: &mut Cx, root: &str) -> Result<Value> {
382 cx.require(&table_fs_capability())?;
383 let dir = FsDir::open(PathBuf::from(root))?;
384 cx.factory().opaque(Arc::new(dir))
385}