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