Skip to main content

atmos/os_lib/aura/builtins/
os_fs.rs

1// 分割: aura/builtins.rs より機械的に移動(2026-07-16 リファクタ フェーズ6)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5/// 文字境界を尊重して先頭 max バイトまでを返す(ログ用の安全トリム・UTF-8 途中で切らない)。
6pub(crate) fn head_str(s: &str, max: usize) -> &str {
7    let mut c = max.min(s.len());
8    while c > 0 && !s.is_char_boundary(c) {
9        c -= 1;
10    }
11    s.get(..c).unwrap_or("")
12}
13
14pub(crate) fn unquote(v: &Value) -> String {
15    let s = v.to_string();
16    if s.starts_with('"') && s.ends_with('"') {
17        s.get(1..s.len() - 1).map(String::from).unwrap_or(s)
18    } else {
19        s
20    }
21}
22
23pub(crate) fn resolve_path(env: &Env, path: &str) -> Result<String, String> {
24    let mut abs_path = if path.starts_with('/') {
25        String::from(path)
26    } else {
27        if let Some(Value::Str(cwd)) = env.get("cwd") {
28            if cwd.ends_with('/') {
29                alloc::format!("{}{}", cwd, path)
30            } else {
31                alloc::format!("{}/{}", cwd, path)
32            }
33        } else {
34            alloc::format!("/user/{}", path) // Fallback
35        }
36    };
37
38    // サンドボックスによるパス解決と権限チェック
39    if let Some(sandbox) = &env.sandbox {
40        if let Some(safe_path) = sandbox.resolve_path(&abs_path) {
41            abs_path = safe_path;
42        } else {
43            return Err(alloc::format!(
44                "Permission denied: Access to '{}' is restricted by the sandbox",
45                abs_path
46            ));
47        }
48    }
49
50    Ok(abs_path)
51}
52
53pub(crate) fn builtin_ls(env: &mut Env, args: &[AST]) -> Result<Value, String> {
54    let vals = eval_args(env, args)?;
55    let mut show_hidden = false;
56    let mut show_details = false;
57    let mut target_dir = if let Some(Value::Str(c)) = env.get("cwd") {
58        c.clone()
59    } else {
60        String::from("/user/")
61    };
62
63    for val in vals {
64        match val {
65            Value::Str(s) => {
66                let unquoted = unquote(&Value::Str(s));
67                if unquoted.starts_with('-') {
68                    if unquoted.contains('a') {
69                        show_hidden = true;
70                    }
71                    if unquoted.contains('l') {
72                        show_details = true;
73                    }
74                } else {
75                    target_dir = unquoted;
76                }
77            }
78            _ => return Err(String::from("Invalid argument for ls")),
79        }
80    }
81
82    let safe_cwd = resolve_path(env, &target_dir)?;
83    let fs = crate::kernel::fs::get_fs();
84    let files = fs.list_dir(&safe_cwd);
85    let mut out = alloc::format!("Listing {}:\n", safe_cwd);
86    for file in files {
87        let name = file.get_filename();
88        let simple_name = if let Some(idx) = name.rfind('/') {
89            name.get(idx + 1..).unwrap_or(name)
90        } else {
91            name
92        };
93        if !show_hidden && simple_name.starts_with('.') {
94            continue;
95        }
96        let f_size = file.size;
97        if show_details {
98            out.push_str(&alloc::format!(
99                " - {} ({} bytes) labels:[{}]\n",
100                simple_name,
101                f_size,
102                file.get_labels()
103            ));
104        } else {
105            out.push_str(&alloc::format!(" - {} ({})\n", simple_name, f_size));
106        }
107    }
108    Ok(Value::Str(out))
109}
110
111pub(crate) fn builtin_cd(env: &mut Env, args: &[AST]) -> Result<Value, String> {
112    if args.is_empty() {
113        return Err(String::from("USAGE: cd(\"path\")"));
114    }
115    let path = unquote(&eval(env, &args[0])?);
116    let new_path = resolve_path(env, &path)?;
117    env.set(String::from("cwd"), Value::Str(new_path.clone()));
118    Ok(Value::Str(alloc::format!("Changed dir to {}", new_path)))
119}
120
121pub(crate) fn builtin_pwd(env: &mut Env, _args: &[AST]) -> Result<Value, String> {
122    let cwd = if let Some(Value::Str(c)) = env.get("cwd") {
123        c.clone()
124    } else {
125        String::from("/user/")
126    };
127    Ok(Value::Str(cwd))
128}
129
130pub(crate) fn builtin_mkdir(env: &mut Env, args: &[AST]) -> Result<Value, String> {
131    let vals = eval_args(env, args)?;
132    if vals.is_empty() {
133        return Err(String::from("USAGE: mkdir(\"path\")"));
134    }
135    let raw_path = unquote(&vals[0]);
136    let path = resolve_path(env, &raw_path)?;
137
138    let keep_file = if path.ends_with('/') {
139        alloc::format!("{}.keep", path)
140    } else {
141        alloc::format!("{}/.keep", path)
142    };
143
144    let fs = crate::kernel::fs::get_fs();
145    if fs.save_file(&keep_file, &[], "dir").is_ok() {
146        Ok(Value::Str(alloc::format!(
147            "SUCCESS: Created directory '{}'",
148            path
149        )))
150    } else {
151        Err(String::from("ERROR: Failed to create directory"))
152    }
153}
154
155pub(crate) fn builtin_cp(env: &mut Env, args: &[AST]) -> Result<Value, String> {
156    let vals = eval_args(env, args)?;
157    if vals.len() < 2 {
158        return Err(String::from("USAGE: cp(\"src\" \"dst\")"));
159    }
160    let src_raw = unquote(&vals[0]);
161    let dst_raw = unquote(&vals[1]);
162    let src_path = resolve_path(env, &src_raw)?;
163    let dst_path = resolve_path(env, &dst_raw)?;
164
165    let fs = crate::kernel::fs::get_fs();
166    if let Some((meta, content)) = fs.read_file(&src_path) {
167        if fs.save_file(&dst_path, &content, meta.get_labels()).is_ok() {
168            Ok(Value::Str(alloc::format!(
169                "SUCCESS: Copied '{}' to '{}'",
170                src_path,
171                dst_path
172            )))
173        } else {
174            Err(String::from("ERROR: Failed to save destination file"))
175        }
176    } else {
177        Err(alloc::format!(
178            "ERROR: Source file '{}' not found",
179            src_path
180        ))
181    }
182}
183
184pub(crate) fn builtin_mv(env: &mut Env, args: &[AST]) -> Result<Value, String> {
185    let vals = eval_args(env, args)?;
186    if vals.len() < 2 {
187        return Err(String::from("USAGE: mv(\"src\" \"dst\")"));
188    }
189    let src_raw = unquote(&vals[0]);
190    let dst_raw = unquote(&vals[1]);
191    let src_path = resolve_path(env, &src_raw)?;
192    let dst_path = resolve_path(env, &dst_raw)?;
193
194    let fs = crate::kernel::fs::get_fs();
195    if let Some((meta, content)) = fs.read_file(&src_path) {
196        if fs.save_file(&dst_path, &content, meta.get_labels()).is_ok() {
197            // 【2026-07-28】元ファイルの削除失敗を捨てると、移動したはずの
198            // ファイルが両方に残る(=コピーになる)のに "Moved" と表示していた。
199            if let Err(e) = fs.delete_file(&src_path) {
200                return Err(alloc::format!(
201                    "ERROR: Copied to '{}' but could not remove '{}': {}",
202                    dst_path,
203                    src_path,
204                    e
205                ));
206            }
207            Ok(Value::Str(alloc::format!(
208                "SUCCESS: Moved '{}' to '{}'",
209                src_path,
210                dst_path
211            )))
212        } else {
213            Err(String::from("ERROR: Failed to save destination file"))
214        }
215    } else {
216        Err(alloc::format!(
217            "ERROR: Source file '{}' not found",
218            src_path
219        ))
220    }
221}
222
223pub(crate) fn builtin_rm(env: &mut Env, args: &[AST]) -> Result<Value, String> {
224    let vals = eval_args(env, args)?;
225    if vals.is_empty() {
226        return Err(String::from("USAGE: rm(\"path\")"));
227    }
228    let raw_path = unquote(&vals[0]);
229    let path = resolve_path(env, &raw_path)?;
230
231    let fs = crate::kernel::fs::get_fs();
232    if fs.delete_file(&path).is_ok() {
233        Ok(Value::Str(alloc::format!(
234            "SUCCESS: Removed file '{}'",
235            path
236        )))
237    } else {
238        Err(alloc::format!(
239            "ERROR: Failed to remove file '{}' or not found",
240            path
241        ))
242    }
243}
244
245pub(crate) fn builtin_cat(env: &mut Env, args: &[AST]) -> Result<Value, String> {
246    let vals = eval_args(env, args)?;
247    if vals.is_empty() {
248        return Err(String::from("USAGE: cat(\"path\")"));
249    }
250    let raw_path = unquote(&vals[0]);
251    let path = resolve_path(env, &raw_path)?;
252
253    let fs = crate::kernel::fs::get_fs();
254    if let Some((_, content)) = fs.read_file(&path) {
255        let content_str = core::str::from_utf8(&content)
256            .map_err(|_| String::from("ERROR: Binary or invalid UTF-8 content"))?;
257        Ok(Value::Str(String::from(content_str)))
258    } else {
259        Err(alloc::format!("ERROR: File '{}' not found", path))
260    }
261}
262
263pub(crate) fn builtin_grep(env: &mut Env, args: &[AST]) -> Result<Value, String> {
264    let vals = eval_args(env, args)?;
265    if vals.len() < 2 {
266        return Err(String::from("USAGE: grep(\"pattern\" \"path\")"));
267    }
268    let pattern = unquote(&vals[0]);
269    let raw_path = unquote(&vals[1]);
270    let path = resolve_path(env, &raw_path)?;
271
272    let fs = crate::kernel::fs::get_fs();
273    if let Some((_, content)) = fs.read_file(&path) {
274        let content_str = core::str::from_utf8(&content)
275            .map_err(|_| String::from("ERROR: Binary or invalid UTF-8 content"))?;
276
277        let mut matching_lines = String::new();
278        for (i, line) in content_str.lines().enumerate() {
279            if line.contains(&pattern) {
280                matching_lines.push_str(&alloc::format!("{}: {}\n", i + 1, line));
281            }
282        }
283        Ok(Value::Str(matching_lines))
284    } else {
285        Err(alloc::format!("ERROR: File '{}' not found", path))
286    }
287}
288
289pub(crate) fn builtin_head(env: &mut Env, args: &[AST]) -> Result<Value, String> {
290    let vals = eval_args(env, args)?;
291    if vals.is_empty() {
292        return Err(String::from("USAGE: head(\"path\" [lines])"));
293    }
294    let raw_path = unquote(&vals[0]);
295    let path = resolve_path(env, &raw_path)?;
296
297    let mut num_lines = 10;
298    if vals.len() > 1 {
299        if let Value::Num(n) = &vals[1] {
300            num_lines = n.to_string().parse::<usize>().unwrap_or(10);
301        }
302    }
303
304    let fs = crate::kernel::fs::get_fs();
305    if let Some((_, content)) = fs.read_file(&path) {
306        let content_str = core::str::from_utf8(&content)
307            .map_err(|_| String::from("ERROR: Binary or invalid UTF-8 content"))?;
308
309        let mut out = String::new();
310        for line in content_str.lines().take(num_lines) {
311            out.push_str(line);
312            out.push('\n');
313        }
314        Ok(Value::Str(out))
315    } else {
316        Err(alloc::format!("ERROR: File '{}' not found", path))
317    }
318}
319
320pub(crate) fn builtin_tail(env: &mut Env, args: &[AST]) -> Result<Value, String> {
321    let vals = eval_args(env, args)?;
322    if vals.is_empty() {
323        return Err(String::from("USAGE: tail(\"path\" [lines])"));
324    }
325    let raw_path = unquote(&vals[0]);
326    let path = resolve_path(env, &raw_path)?;
327
328    let mut num_lines = 10;
329    if vals.len() > 1 {
330        if let Value::Num(n) = &vals[1] {
331            num_lines = n.to_string().parse::<usize>().unwrap_or(10);
332        }
333    }
334
335    let fs = crate::kernel::fs::get_fs();
336    if let Some((_, content)) = fs.read_file(&path) {
337        let content_str = core::str::from_utf8(&content)
338            .map_err(|_| String::from("ERROR: Binary or invalid UTF-8 content"))?;
339
340        let lines: Vec<&str> = content_str.lines().collect();
341        let start = if lines.len() > num_lines {
342            lines.len() - num_lines
343        } else {
344            0
345        };
346
347        let mut out = String::new();
348        for line in &lines[start..] {
349            out.push_str(line);
350            out.push('\n');
351        }
352        Ok(Value::Str(out))
353    } else {
354        Err(alloc::format!("ERROR: File '{}' not found", path))
355    }
356}
357
358pub(crate) fn builtin_save(env: &mut Env, args: &[AST]) -> Result<Value, String> {
359    let vals = eval_args(env, args)?;
360    if vals.len() < 2 {
361        return Err(String::from("USAGE: save(\"name\" \"data\" [\"labels\"])"));
362    }
363    let raw_filename = unquote(&vals[0]);
364    let filename = resolve_path(env, &raw_filename)?;
365    let content = unquote(&vals[1]);
366    let labels = if vals.len() > 2 {
367        unquote(&vals[2])
368    } else {
369        String::new()
370    };
371
372    let fs = crate::kernel::fs::get_fs();
373    if fs.save_file(&filename, content.as_bytes(), &labels).is_ok() {
374        Ok(Value::Str(alloc::format!(
375            "SUCCESS: Saved file '{}'",
376            filename
377        )))
378    } else {
379        Err(String::from("ERROR: Failed to save file"))
380    }
381}
382
383pub(crate) fn builtin_show(env: &mut Env, args: &[AST]) -> Result<Value, String> {
384    let vals = eval_args(env, args)?;
385    if vals.is_empty() {
386        return Err(String::from("USAGE: show(\"filename\")"));
387    }
388    let raw_filename = unquote(&vals[0]);
389    let filename = resolve_path(env, &raw_filename)?;
390
391    let fs = crate::kernel::fs::get_fs();
392    if let Some((meta, content)) = fs.read_file(&filename) {
393        let content_str = unsafe { core::str::from_utf8_unchecked(&content) };
394        Ok(Value::Str(alloc::format!(
395            "FILE: '{}' | LABELS: [{}]\nDATA: {}",
396            filename,
397            meta.get_labels(),
398            content_str
399        )))
400    } else {
401        Err(String::from("ERROR: File not found or corrupt!"))
402    }
403}
404
405pub(crate) fn builtin_find(env: &mut Env, args: &[AST]) -> Result<Value, String> {
406    let vals = eval_args(env, args)?;
407    if vals.is_empty() {
408        return Err(String::from("USAGE: find(\"label\")"));
409    }
410    let label = unquote(&vals[0]);
411
412    let fs = crate::kernel::fs::get_fs();
413    let files = fs.find_by_label(&label);
414    if !files.is_empty() {
415        let mut out = alloc::format!("FOUND {} file(s) for label '{}':\n", files.len(), label);
416        for file in files {
417            let f_size = file.size;
418            out.push_str(&alloc::format!(
419                " - {} ({} bytes) labels:[{}]\n",
420                file.get_filename(),
421                f_size,
422                file.get_labels()
423            ));
424        }
425        Ok(Value::Str(out))
426    } else {
427        Ok(Value::Str(alloc::format!(
428            "No files found with label '{}'",
429            label
430        )))
431    }
432}
433
434pub(crate) fn builtin_addlabel(env: &mut Env, args: &[AST]) -> Result<Value, String> {
435    let vals = eval_args(env, args)?;
436    if vals.len() < 2 {
437        return Err(String::from("USAGE: addlabel(\"filename\" \"label\")"));
438    }
439    let raw_filename = unquote(&vals[0]);
440    let filename = resolve_path(env, &raw_filename)?;
441    let new_label = unquote(&vals[1]);
442
443    let fs = crate::kernel::fs::get_fs();
444    if let Some((meta, content)) = fs.read_file(&filename) {
445        let old_labels = meta.get_labels();
446        let joined_labels = if old_labels.is_empty() {
447            new_label.clone()
448        } else {
449            alloc::format!("{},{}", old_labels, new_label)
450        };
451        if fs.save_file(&filename, &content, &joined_labels).is_ok() {
452            Ok(Value::Str(alloc::format!(
453                "SUCCESS: Added label '{}' to '{}'",
454                new_label,
455                filename
456            )))
457        } else {
458            Err(String::from("ERROR: Failed to save updated file"))
459        }
460    } else {
461        Err(String::from("ERROR: File not found"))
462    }
463}
464
465pub(crate) fn builtin_corrupt(env: &mut Env, args: &[AST]) -> Result<Value, String> {
466    let vals = eval_args(env, args)?;
467    if vals.is_empty() {
468        return Err(String::from("USAGE: corrupt(sector)"));
469    }
470
471    let sector = match &vals[0] {
472        Value::Num(n) => {
473            let s = n.to_string();
474            s.parse::<usize>().unwrap_or(0)
475        }
476        v => {
477            let s = unquote(v);
478            s.parse::<usize>().unwrap_or(0)
479        }
480    };
481
482    let fs = crate::kernel::fs::get_fs();
483    if fs.device.force_corrupt_raw_sector(sector).is_ok() {
484        Ok(Value::Str(alloc::format!(
485            "DEBUG: Sector {} on RAMDisk corrupted successfully!",
486            sector
487        )))
488    } else {
489        Err(String::from("ERROR: Failed to corrupt sector"))
490    }
491}
492
493pub(crate) fn builtin_keymap(env: &mut Env, args: &[AST]) -> Result<Value, String> {
494    let vals = eval_args(env, args)?;
495    if let Some(ctx) = &env.context {
496        if vals.is_empty() {
497            let app = crate::apps::keymap::KeymapApp::new();
498            let win = crate::kernel::window_mgr::Window::new(
499                200,
500                150,
501                400,
502                500,
503                "Keymap & Checker",
504                alloc::boxed::Box::new(app),
505            );
506            crate::kernel::window_mgr::get_instance().add_window(win);
507            return Ok(Value::Str(String::from("Launched Keymap App")));
508        }
509        let layout_name = unquote(&vals[0]);
510        if let Some(layout) = crate::kernel::keyboard::KeyboardLayout::parse(&layout_name) {
511            unsafe {
512                let kbd = &mut **ctx;
513                kbd.set_layout(layout);
514            }
515            Ok(Value::Str(alloc::format!(
516                "SUCCESS: Keyboard layout switched to {}",
517                layout_name
518            )))
519        } else {
520            Err(String::from("USAGE: keymap(\"us\"|\"js\")"))
521        }
522    } else {
523        Err(String::from("OS Context not available for keymap command"))
524    }
525}
526
527// ====================================================================
528// OS Network Commands
529// ====================================================================
530