rew_ops/
lib.rs

1use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
2use deno_core::OpState;
3use deno_core::error::CoreError;
4use deno_core::op2;
5use rew_core::RuntimeState;
6use rew_core::utils::find_app_path;
7use rew_data_manager::{DataFormat, DataManager};
8use rew_vfile::{VIRTUAL_FILES, add_virtual_file};
9use serde::{Deserialize, Serialize};
10use serde_yaml;
11use std::cell::RefCell;
12use std::collections::HashMap;
13use std::fs::{self, File};
14use std::io::{self, Read, Write};
15use std::path::{Path, PathBuf};
16use std::rc::Rc;
17
18#[op2]
19#[serde]
20pub fn op_get_args(state: Rc<RefCell<OpState>>) -> Result<serde_json::Value, CoreError> {
21  let state = state.borrow();
22  let runtime_args = state.borrow::<RuntimeState>();
23  Ok(serde_json::json!(runtime_args.args.clone()))
24}
25
26// Base64 encoding/decoding operations
27#[op2]
28#[string]
29pub fn op_to_base64(#[serde] data: serde_json::Value) -> Result<String, CoreError> {
30  match data {
31    serde_json::Value::String(text) => Ok(BASE64.encode(text.as_bytes())),
32    serde_json::Value::Array(bytes) => {
33      let buffer: Result<Vec<u8>, _> = bytes
34        .iter()
35        .map(|v| {
36          if let serde_json::Value::Number(n) = v {
37            n.as_u64().map(|n| n as u8).ok_or_else(|| {
38              CoreError::Io(io::Error::new(
39                io::ErrorKind::InvalidData,
40                "Invalid byte value",
41              ))
42            })
43          } else {
44            Err(CoreError::Io(io::Error::new(
45              io::ErrorKind::InvalidData,
46              "Expected number in byte array",
47            )))
48          }
49        })
50        .collect();
51
52      match buffer {
53        Ok(bytes) => Ok(BASE64.encode(bytes)),
54        Err(e) => Err(e),
55      }
56    }
57    _ => Err(CoreError::Io(io::Error::new(
58      io::ErrorKind::InvalidData,
59      "Expected string or array of bytes for base64 encoding",
60    ))),
61  }
62}
63
64#[op2]
65#[serde]
66pub fn op_from_base64(
67  #[string] encoded: String,
68  #[serde] options: Option<Base64DecodeOptions>,
69) -> Result<serde_json::Value, CoreError> {
70  let options = options.unwrap_or_default();
71
72  let decoded = BASE64
73    .decode(encoded.as_bytes())
74    .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::InvalidData, e)))?;
75
76  if options.as_string {
77    let text = String::from_utf8(decoded)
78      .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::InvalidData, e)))?;
79    Ok(serde_json::Value::String(text))
80  } else {
81    Ok(serde_json::Value::Array(
82      decoded
83        .into_iter()
84        .map(|b| serde_json::Value::Number(b.into()))
85        .collect(),
86    ))
87  }
88}
89
90#[derive(Deserialize, Default)]
91struct Base64DecodeOptions {
92  as_string: bool,
93}
94
95#[op2]
96#[string]
97pub fn op_find_app(
98  #[string] filepath: String,
99  _: Rc<RefCell<OpState>>,
100) -> Result<String, CoreError> {
101  let current_file = Path::new(&filepath);
102
103  let app_path = find_app_path(current_file);
104
105  Ok(String::from(
106    app_path.unwrap_or(PathBuf::from("")).to_str().unwrap(),
107  ))
108}
109
110#[op2]
111#[string]
112pub fn op_yaml_to_string(
113  #[serde] data: serde_json::Value,
114  _: Rc<RefCell<OpState>>,
115) -> Result<String, CoreError> {
116  let yaml = serde_yaml::to_string(&data)
117    .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::InvalidData, e)))?;
118
119  Ok(yaml)
120}
121
122#[op2]
123#[serde]
124pub fn op_string_to_yaml(
125  #[string] yaml_str: String,
126  _: Rc<RefCell<OpState>>,
127) -> Result<serde_json::Value, CoreError> {
128  let value: serde_json::Value = serde_yaml::from_str(&yaml_str)
129    .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::InvalidData, e)))?;
130
131  Ok(value)
132}
133
134#[op2]
135#[serde]
136pub fn op_app_loadconfig(
137  #[string] app_path: String,
138  _: Rc<RefCell<OpState>>,
139) -> Result<serde_json::Value, CoreError> {
140  let app_path = Path::new(&app_path);
141
142  if !app_path.exists() {
143    return Err(CoreError::Io(io::Error::new(
144      io::ErrorKind::NotFound,
145      format!("App path not found: {}", app_path.display()),
146    )));
147  }
148
149  let config_path = app_path.join("app.yaml");
150
151  if !config_path.exists() {
152    return Err(CoreError::Io(io::Error::new(
153      io::ErrorKind::NotFound,
154      format!("App config not found: {}", config_path.display()),
155    )));
156  }
157
158  let config_str = fs::read_to_string(&config_path)
159    .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::Other, e)))?;
160
161  let config: serde_json::Value = serde_yaml::from_str(&config_str)
162    .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::InvalidData, e)))?;
163
164  Ok(config)
165}
166
167// Helper function to get DataManager for a specific app package
168pub fn get_data_manager_for_package(app_package: &str) -> Result<DataManager, CoreError> {
169  // For now, use "default" as the user ID
170  // In a real implementation, you'd get this from user authentication
171  let user_id = "default";
172
173  DataManager::new(user_id, app_package)
174    .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::Other, e)))
175}
176
177#[op2]
178#[string]
179pub fn op_data_read(
180  #[string] app_package: String,
181  #[string] key: String,
182  _: Rc<RefCell<OpState>>,
183) -> Result<String, CoreError> {
184  let data_manager = get_data_manager_for_package(&app_package)?;
185  data_manager
186    .read(&key)
187    .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::Other, e)))
188}
189
190#[op2(async)]
191pub async fn op_data_write(
192  #[string] app_package: String,
193  #[string] key: String,
194  #[string] content: String,
195  _: Rc<RefCell<OpState>>,
196) -> Result<(), CoreError> {
197  let data_manager = get_data_manager_for_package(&app_package)?;
198  data_manager
199    .write(&key, &content)
200    .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::Other, e)))
201}
202
203#[op2(async)]
204pub async fn op_data_delete(
205  #[string] app_package: String,
206  #[string] key: String,
207  _: Rc<RefCell<OpState>>,
208) -> Result<(), CoreError> {
209  let data_manager = get_data_manager_for_package(&app_package)?;
210  data_manager
211    .delete(&key)
212    .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::Other, e)))
213}
214
215#[op2(fast)]
216pub fn op_data_exists(
217  #[string] app_package: String,
218  #[string] key: String,
219  _: Rc<RefCell<OpState>>,
220) -> Result<bool, CoreError> {
221  let data_manager = get_data_manager_for_package(&app_package)?;
222  Ok(data_manager.exists(&key))
223}
224
225#[op2]
226#[string]
227pub fn op_data_list(
228  #[string] app_package: String,
229  #[string] prefix: String,
230  _: Rc<RefCell<OpState>>,
231) -> Result<String, CoreError> {
232  let data_manager = get_data_manager_for_package(&app_package)?;
233  let files = data_manager
234    .list(&prefix)
235    .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::Other, e)))?;
236
237  serde_json::to_string(&files).map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::Other, e)))
238}
239
240#[op2]
241#[serde]
242pub fn op_data_read_binary(
243  #[string] app_package: String,
244  #[string] key: String,
245  _: Rc<RefCell<OpState>>,
246) -> Result<Vec<u8>, CoreError> {
247  let data_manager = get_data_manager_for_package(&app_package)?;
248  data_manager
249    .read_binary(&key)
250    .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::Other, e)))
251}
252
253#[op2]
254#[string]
255pub fn op_fetch_env(_: Rc<RefCell<OpState>>) -> Result<String, CoreError> {
256  let env_vars: HashMap<String, String> = std::env::vars().collect();
257  let cwd = std::env::current_dir()?
258    // .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::Other, e)))?;
259    .to_string_lossy()
260    .to_string();
261  let exec_path = std::env::current_exe()?
262    // .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::Other, e)))?;
263    .to_string_lossy()
264    .to_string();
265
266  let result = serde_json::json!({
267    "env": env_vars,
268    "cwd": cwd,
269    "execPath": exec_path,
270    "tempDir": std::env::temp_dir(),
271    "rewPath": rew_core::utils::get_rew_root()
272  });
273
274  serde_json::to_string(&result)
275    .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::InvalidData, e)))
276}
277
278#[op2(async)]
279pub async fn op_data_write_binary(
280  #[string] app_package: String,
281  #[string] key: String,
282  #[serde] data: Vec<u8>,
283  _: Rc<RefCell<OpState>>,
284) -> Result<(), CoreError> {
285  let data_manager = get_data_manager_for_package(&app_package)?;
286  data_manager
287    .write_binary(&key, &data)
288    .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::Other, e)))
289}
290
291#[op2]
292#[serde]
293pub fn op_data_read_yaml(
294  #[string] app_package: String,
295  #[string] key: String,
296  _: Rc<RefCell<OpState>>,
297) -> Result<serde_json::Value, CoreError> {
298  let data_manager = get_data_manager_for_package(&app_package)?;
299  data_manager
300    .read_yaml(&key)
301    .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::Other, e)))
302}
303
304#[op2(async)]
305pub async fn op_data_write_yaml(
306  #[string] app_package: String,
307  #[string] key: String,
308  #[serde] data: serde_json::Value,
309  _: Rc<RefCell<OpState>>,
310) -> Result<(), CoreError> {
311  let data_manager = get_data_manager_for_package(&app_package)?;
312  data_manager
313    .write_yaml(&key, &data)
314    .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::Other, e)))
315}
316
317#[op2]
318#[serde]
319pub fn op_data_get_info(
320  #[string] app_package: String,
321  #[string] key: String,
322  _: Rc<RefCell<OpState>>,
323) -> Result<(bool, String), CoreError> {
324  let data_manager = get_data_manager_for_package(&app_package)?;
325  let (exists, format) = data_manager
326    .get_file_info(&key)
327    .map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::Other, e)))?;
328
329  let format_str = match format {
330    DataFormat::Text => "text",
331    DataFormat::Json => "json",
332    DataFormat::Yaml => "yaml",
333    DataFormat::Binary => "binary",
334  };
335
336  Ok((exists, format_str.to_string()))
337}
338
339#[op2]
340#[string]
341pub fn op_data_get_path(
342  #[string] app_package: String,
343  _: Rc<RefCell<OpState>>,
344) -> Result<String, CoreError> {
345  let data_manager = get_data_manager_for_package(&app_package)?;
346
347  Ok(data_manager.get_path("").to_string_lossy().to_string())
348}
349
350#[op2]
351#[string]
352pub fn op_os_info_os(_: Rc<RefCell<OpState>>) -> Result<String, CoreError> {
353  Ok(std::env::consts::OS.to_string())
354}
355
356#[op2]
357#[string]
358pub fn op_os_info_arch(_: Rc<RefCell<OpState>>) -> Result<String, CoreError> {
359  Ok(std::env::consts::ARCH.to_string())
360}
361
362#[op2]
363#[string]
364pub fn op_os_info_family(_: Rc<RefCell<OpState>>) -> Result<String, CoreError> {
365  Ok(std::env::consts::FAMILY.to_string())
366}
367
368use rand::rngs::StdRng;
369use rand::{Rng, RngCore, SeedableRng, distributions::Alphanumeric};
370use std::hash::Hash;
371use std::hash::Hasher;
372
373#[op2]
374#[serde]
375pub fn op_rand_from(
376  #[bigint] min: usize,
377  #[bigint] max: usize,
378  #[string] seed: Option<String>,
379) -> usize {
380  let mut rng: Box<dyn RngCore> = match seed {
381    Some(s) => {
382      let mut hasher = std::collections::hash_map::DefaultHasher::new();
383      s.hash(&mut hasher);
384      Box::new(StdRng::seed_from_u64(hasher.finish()))
385    }
386    _ => Box::new(rand::thread_rng()),
387  };
388
389  if min == max {
390    return min;
391  }
392
393  let (low, high) = if min < max { (min, max) } else { (max, min) };
394
395  rng.gen_range(low..=high)
396}
397
398#[op2]
399#[string]
400pub fn op_vfile_set(#[string] full_path: String, #[string] content: String) -> String {
401  add_virtual_file(full_path.as_str(), content.as_str());
402  "".to_string()
403}
404
405#[op2]
406#[string]
407pub fn op_vfile_get(#[string] full_path: String) -> String {
408  if let Some(v) = VIRTUAL_FILES
409    .lock()
410    .unwrap()
411    .iter()
412    .find(|(p, _)| *p == full_path)
413  {
414    return v.1.clone();
415  }
416  "".to_string()
417}
418
419#[op2]
420#[string]
421pub fn op_gen_uid(length: i32, #[string] seed: Option<String>) -> String {
422  if let Some(seed_str) = seed {
423    let mut hasher = std::collections::hash_map::DefaultHasher::new();
424    seed_str.hash(&mut hasher);
425
426    let seed = hasher.finish();
427    let mut rng = StdRng::seed_from_u64(seed);
428
429    (0..length)
430      .map(|_| rng.sample(Alphanumeric) as char)
431      .collect()
432  } else {
433    let mut rng = rand::thread_rng();
434
435    (0..length)
436      .map(|_| rng.sample(Alphanumeric) as char)
437      .collect()
438  }
439}
440
441#[op2]
442#[serde]
443pub fn op_terminal_size() -> Result<(u16, u16), std::io::Error> {
444  #[cfg(unix)]
445  {
446    use libc::{STDOUT_FILENO, TIOCGWINSZ, ioctl, winsize};
447
448    let mut ws: winsize = unsafe { std::mem::zeroed() };
449
450    let result = unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ, &mut ws) };
451
452    if result == -1 {
453      return Err(std::io::Error::last_os_error());
454    }
455
456    Ok((ws.ws_col, ws.ws_row))
457  }
458
459  #[cfg(windows)]
460  {
461    use std::mem::zeroed;
462    use std::ptr::null_mut;
463    use winapi::um::handleapi::INVALID_HANDLE_VALUE;
464    use winapi::um::processenv::GetStdHandle;
465    use winapi::um::winbase::STD_OUTPUT_HANDLE;
466    use winapi::um::wincon::{CONSOLE_SCREEN_BUFFER_INFO, GetConsoleScreenBufferInfo};
467
468    unsafe {
469      let handle = GetStdHandle(STD_OUTPUT_HANDLE);
470      if handle == INVALID_HANDLE_VALUE {
471        return Err(std::io::Error::last_os_error());
472      }
473
474      let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = zeroed();
475      if GetConsoleScreenBufferInfo(handle, &mut csbi) == 0 {
476        return Err(std::io::Error::last_os_error());
477      }
478
479      let width = (csbi.srWindow.Right - csbi.srWindow.Left + 1) as u16;
480      let height = (csbi.srWindow.Bottom - csbi.srWindow.Top + 1) as u16;
481
482      Ok((width, height))
483    }
484  }
485}
486
487#[op2]
488#[serde]
489pub fn op_fs_read(
490  #[string] current_file: String,
491  #[string] filepath: String,
492  #[serde] options: Option<ReadOptions>,
493  _: Rc<RefCell<OpState>>,
494) -> Result<serde_json::Value, CoreError> {
495  let current_file_path = Path::new(&current_file);
496  let base_dir = current_file_path.parent().unwrap_or(Path::new("."));
497  let full_path = base_dir.join(filepath);
498
499  let options = options.unwrap_or_default();
500
501  if options.binary {
502    let mut file = File::open(&full_path).map_err(CoreError::Io)?;
503    let mut buffer = Vec::new();
504    file.read_to_end(&mut buffer).map_err(CoreError::Io)?;
505
506    Ok(serde_json::Value::Array(
507      buffer
508        .into_iter()
509        .map(|b| serde_json::Value::Number(b.into()))
510        .collect(),
511    ))
512  } else {
513    let content = fs::read_to_string(&full_path).map_err(CoreError::Io)?;
514    Ok(serde_json::Value::String(content))
515  }
516}
517
518#[derive(Deserialize, Default)]
519struct ReadOptions {
520  binary: bool,
521}
522
523#[op2(async)]
524pub async fn op_fs_write(
525  #[string] current_file: String,
526  #[string] filepath: String,
527  #[serde] content: serde_json::Value,
528  #[serde] options: Option<WriteOptions>,
529  _: Rc<RefCell<OpState>>,
530) -> Result<(), CoreError> {
531  let current_file_path = Path::new(&current_file);
532  let base_dir = current_file_path.parent().unwrap_or(Path::new("."));
533
534  let full_path = base_dir.join(filepath);
535
536  let options = options.unwrap_or_default();
537
538  if let Some(parent) = full_path.parent() {
539    if options.create_dirs {
540      fs::create_dir_all(parent).map_err(CoreError::Io)?;
541    }
542  }
543
544  if options.binary {
545    if let serde_json::Value::Array(bytes) = content {
546      let buffer: Result<Vec<u8>, _> = bytes
547        .iter()
548        .map(|v| {
549          if let serde_json::Value::Number(n) = v {
550            n.as_u64().map(|n| n as u8).ok_or_else(|| {
551              CoreError::Io(io::Error::new(
552                io::ErrorKind::InvalidData,
553                "Invalid byte value",
554              ))
555            })
556          } else {
557            Err(CoreError::Io(io::Error::new(
558              io::ErrorKind::InvalidData,
559              "Expected number in byte array",
560            )))
561          }
562        })
563        .collect();
564
565      fs::write(&full_path, buffer?).map_err(CoreError::Io)?;
566    } else {
567      return Err(CoreError::Io(io::Error::new(
568        io::ErrorKind::InvalidData,
569        "Expected array of bytes for binary write",
570      )));
571    }
572  } else if let serde_json::Value::String(text) = content {
573    let mut file = File::create(&full_path).map_err(CoreError::Io)?;
574    file.write_all(text.as_bytes()).map_err(CoreError::Io)?;
575  } else {
576    return Err(CoreError::Io(io::Error::new(
577      io::ErrorKind::InvalidData,
578      "Expected string for text write",
579    )));
580  }
581
582  Ok(())
583}
584
585#[derive(Deserialize, Default)]
586struct WriteOptions {
587  binary: bool,
588  create_dirs: bool,
589}
590
591use sha2::{Digest, Sha256};
592#[op2]
593#[string]
594pub fn op_fs_sha(
595  #[string] current_file: String,
596  #[string] filepath: String,
597  _: Rc<RefCell<OpState>>,
598) -> Result<String, CoreError> {
599  let current_file_path = Path::new(&current_file);
600  let base_dir = current_file_path.parent().unwrap_or(Path::new("."));
601
602  let full_path = base_dir.join(filepath);
603
604  let file_bytes = fs::read(&full_path)?;
605  let mut hasher = Sha256::new();
606  hasher.update(file_bytes);
607  let hash = hasher.finalize();
608
609  Ok(format!("{:x}", hash))
610}
611
612#[op2(fast)]
613pub fn op_fs_exists(
614  #[string] current_file: String,
615  #[string] filepath: String,
616  _: Rc<RefCell<OpState>>,
617) -> Result<bool, CoreError> {
618  let current_file_path = Path::new(&current_file);
619  let base_dir = current_file_path.parent().unwrap_or(Path::new("."));
620
621  let full_path = base_dir.join(filepath);
622
623  Ok(full_path.exists())
624}
625
626#[op2(async)]
627pub async fn op_fs_rm(
628  #[string] current_file: String,
629  #[string] filepath: String,
630  #[serde] options: Option<RemoveOptions>,
631  _: Rc<RefCell<OpState>>,
632) -> Result<(), CoreError> {
633  let current_file_path = Path::new(&current_file);
634  let base_dir = current_file_path.parent().unwrap_or(Path::new("."));
635
636  let full_path = base_dir.join(filepath);
637
638  let options = options.unwrap_or_default();
639
640  if full_path.is_dir() {
641    if options.recursive {
642      fs::remove_dir_all(&full_path).map_err(CoreError::Io)?;
643    } else {
644      fs::remove_dir(&full_path).map_err(CoreError::Io)?;
645    }
646  } else {
647    fs::remove_file(&full_path).map_err(CoreError::Io)?;
648  }
649
650  Ok(())
651}
652
653#[derive(Deserialize, Default)]
654struct RemoveOptions {
655  recursive: bool,
656}
657
658#[op2(async)]
659pub async fn op_fs_mkdir(
660  #[string] current_file: String,
661  #[string] dirpath: String,
662  #[serde] options: Option<MkdirOptions>,
663  _: Rc<RefCell<OpState>>,
664) -> Result<(), CoreError> {
665  let current_file_path = Path::new(&current_file);
666  let base_dir = current_file_path.parent().unwrap_or(Path::new("."));
667
668  let full_path = base_dir.join(dirpath);
669
670  let options = options.unwrap_or_default();
671
672  if options.recursive {
673    fs::create_dir_all(&full_path).map_err(CoreError::Io)?;
674  } else {
675    fs::create_dir(&full_path).map_err(CoreError::Io)?;
676  }
677
678  Ok(())
679}
680
681#[derive(Deserialize, Default)]
682struct MkdirOptions {
683  recursive: bool,
684}
685
686#[op2]
687#[string]
688pub fn op_fs_readdir(
689  #[string] current_file: String,
690  #[string] dirpath: String,
691  #[serde] options: Option<ReaddirOptions>,
692  _: Rc<RefCell<OpState>>,
693) -> Result<String, CoreError> {
694  let current_file_path = Path::new(&current_file);
695  let base_dir = current_file_path.parent().unwrap_or(Path::new("."));
696
697  let full_path = base_dir.join(dirpath);
698
699  let options = options.unwrap_or_default();
700
701  let entries = fs::read_dir(&full_path).map_err(CoreError::Io)?;
702
703  let mut result = Vec::new();
704
705  for entry in entries {
706    let entry = entry.map_err(CoreError::Io)?;
707    let file_type = entry.file_type().map_err(CoreError::Io)?;
708
709    if !options.include_hidden {
710      if let Some(file_name) = entry.path().file_name() {
711        if let Some(name_str) = file_name.to_str() {
712          if name_str.starts_with(".") {
713            continue;
714          }
715        }
716      }
717    }
718
719    if let Some(filter_type) = &options.filter_type {
720      match filter_type.as_str() {
721        "file" => {
722          if !file_type.is_file() {
723            continue;
724          }
725        }
726        "directory" => {
727          if !file_type.is_dir() {
728            continue;
729          }
730        }
731        "symlink" => {
732          if !file_type.is_symlink() {
733            continue;
734          }
735        }
736        _ => {}
737      }
738    }
739
740    let metadata = entry.metadata().map_err(CoreError::Io)?;
741
742    let entry_info = DirEntryInfo {
743      name: entry.file_name().to_string_lossy().to_string(),
744      path: entry.path().to_string_lossy().to_string(),
745      is_file: file_type.is_file(),
746      is_directory: file_type.is_dir(),
747      is_symlink: file_type.is_symlink(),
748      size: metadata.len(),
749      modified: metadata
750        .modified()
751        .ok()
752        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
753        .map(|d| d.as_secs()),
754      created: metadata
755        .created()
756        .ok()
757        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
758        .map(|d| d.as_secs()),
759    };
760
761    result.push(entry_info);
762  }
763
764  if let Some(sort_by) = &options.sort_by {
765    match sort_by.as_str() {
766      "name" => result.sort_by(|a, b| a.name.cmp(&b.name)),
767      "size" => result.sort_by(|a, b| a.size.cmp(&b.size)),
768      "modified" => result.sort_by(|a, b| a.modified.cmp(&b.modified)),
769      "type" => result.sort_by(|a, b| a.is_directory.cmp(&b.is_directory).reverse()),
770      _ => {}
771    }
772  }
773
774  serde_json::to_string(&result).map_err(|e| CoreError::Io(io::Error::new(io::ErrorKind::Other, e)))
775}
776
777#[derive(Deserialize, Default)]
778struct ReaddirOptions {
779  include_hidden: bool,
780  filter_type: Option<String>,
781  sort_by: Option<String>,
782}
783
784#[derive(Serialize)]
785struct DirEntryInfo {
786  name: String,
787  path: String,
788  is_file: bool,
789  is_directory: bool,
790  is_symlink: bool,
791  size: u64,
792  modified: Option<u64>,
793  created: Option<u64>,
794}
795
796#[op2]
797#[string]
798pub fn op_fs_stats(
799  #[string] current_file: String,
800  #[string] filepath: String,
801  _: Rc<RefCell<OpState>>,
802) -> Result<String, CoreError> {
803  let current_file_path = Path::new(&current_file);
804  let base_dir = current_file_path.parent().unwrap_or(Path::new("."));
805
806  let full_path = base_dir.join(filepath);
807
808  let metadata = fs::metadata(&full_path).map_err(CoreError::Io)?;
809
810  let stats = serde_json::json!({
811      "isFile": metadata.is_file(),
812      "isDirectory": metadata.is_dir(),
813      "isSymlink": metadata.file_type().is_symlink(),
814      "size": metadata.len(),
815      "modified": metadata.modified().ok().and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()).map(|d| d.as_secs()),
816      "created": metadata.created().ok().and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()).map(|d| d.as_secs()),
817      "accessed": metadata.accessed().ok().and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()).map(|d| d.as_secs()),
818      "permissions": {
819          "readonly": metadata.permissions().readonly(),
820          // "mode": metadata.permissions().mode(),
821      }
822  });
823
824  Ok(stats.to_string())
825}
826
827#[op2(async)]
828pub async fn op_fs_copy(
829  #[string] current_file: String,
830  #[string] src: String,
831  #[string] dest: String,
832  #[serde] options: Option<CopyOptions>,
833  _: Rc<RefCell<OpState>>,
834) -> Result<(), CoreError> {
835  let current_file_path = Path::new(&current_file);
836  let base_dir = current_file_path.parent().unwrap_or(Path::new("."));
837
838  let src_path = base_dir.join(src);
839  let dest_path = base_dir.join(dest);
840
841  let options = options.unwrap_or_default();
842
843  if src_path.is_dir() {
844    if options.recursive {
845      copy_dir_recursive(&src_path, &dest_path, &options).map_err(CoreError::Io)?;
846    } else {
847      return Err(CoreError::Io(io::Error::new(
848        io::ErrorKind::InvalidInput,
849        "Source is a directory, but recursive option is not set",
850      )));
851    }
852  } else {
853    if let Some(parent) = dest_path.parent() {
854      if options.create_dirs {
855        fs::create_dir_all(parent).map_err(CoreError::Io)?;
856      }
857    }
858
859    fs::copy(&src_path, &dest_path).map_err(CoreError::Io)?;
860  }
861
862  Ok(())
863}
864
865#[derive(Deserialize, Default)]
866pub struct CopyOptions {
867  pub recursive: bool,
868  pub create_dirs: bool,
869  pub overwrite: bool,
870}
871
872pub fn copy_dir_recursive(src: &Path, dest: &Path, options: &CopyOptions) -> io::Result<()> {
873  if !dest.exists() {
874    fs::create_dir_all(dest)?;
875  }
876
877  for entry in fs::read_dir(src)? {
878    let entry = entry?;
879    let src_path = entry.path();
880    let dest_path = dest.join(entry.file_name());
881
882    if src_path.is_dir() {
883      copy_dir_recursive(&src_path, &dest_path, options)?;
884    } else {
885      if dest_path.exists() && !options.overwrite {
886        continue;
887      }
888      fs::copy(&src_path, &dest_path)?;
889    }
890  }
891
892  Ok(())
893}
894
895#[op2(async)]
896pub async fn op_fs_rename(
897  #[string] current_file: String,
898  #[string] src: String,
899  #[string] dest: String,
900  _: Rc<RefCell<OpState>>,
901) -> Result<(), CoreError> {
902  let current_file_path = Path::new(&current_file);
903  let base_dir = current_file_path.parent().unwrap_or(Path::new("."));
904
905  let src_path = base_dir.join(src);
906  let dest_path = base_dir.join(dest);
907
908  fs::rename(&src_path, &dest_path).map_err(CoreError::Io)?;
909
910  Ok(())
911}
912
913#[op2]
914#[string]
915pub fn op_fs_cwdir(state: Rc<RefCell<OpState>>) -> Result<String, CoreError> {
916  let state = state.borrow();
917  let runtime_state = state.borrow::<RuntimeState>();
918
919  Ok(runtime_state.current_dir.to_string_lossy().to_string())
920}
921
922#[op2]
923#[string]
924pub fn op_p_exit(code: i32) -> Result<String, CoreError> {
925  std::process::exit(code);
926}
927
928#[op2]
929#[string]
930pub fn op_p_panic(#[string] msg: String) -> Result<String, CoreError> {
931  panic!("{}", msg);
932}
933
934
935use tokio::time::{sleep, Duration};
936#[op2(async)]
937#[serde]
938pub async fn op_p_sleep(#[bigint] ms: u64) -> Result<(), CoreError> {
939  sleep(Duration::from_millis(ms)).await;
940  Ok(())
941}