1use crate::interpreter::coerce::*;
6use crate::interpreter::value::*;
7use crate::interpreter::{BuiltinContext, Interpreter};
8use derive_more::{Display, Error};
9use itertools::Itertools;
10use minir_macros::{builtin, interpreter_builtin};
11use std::fs;
12use std::path::Path;
13
14#[derive(Debug, Display, Error)]
18#[allow(dead_code)]
19pub enum SystemError {
20 #[display("cannot copy '{}' to '{}': {}", from, to, source)]
21 Copy {
22 from: String,
23 to: String,
24 source: std::io::Error,
25 },
26 #[display("cannot rename '{}' to '{}': {}", from, to, source)]
27 Rename {
28 from: String,
29 to: String,
30 source: std::io::Error,
31 },
32 #[display("cannot remove '{}': {}", path, source)]
33 Remove {
34 path: String,
35 source: std::io::Error,
36 },
37 #[display("cannot create directory '{}': {}", path, source)]
38 CreateDir {
39 path: String,
40 source: std::io::Error,
41 },
42 #[display("cannot read directory '{}': {}", path, source)]
43 ReadDir {
44 path: String,
45 source: std::io::Error,
46 },
47 #[display("cannot execute command '{}': {}", command, source)]
48 Command {
49 command: String,
50 source: std::io::Error,
51 },
52 #[display("cannot get current directory: {}", source)]
53 GetCwd {
54 #[error(source)]
55 source: std::io::Error,
56 },
57 #[display("cannot set current directory '{}': {}", path, source)]
58 SetCwd {
59 path: String,
60 source: std::io::Error,
61 },
62}
63
64impl From<SystemError> for RError {
65 fn from(e: SystemError) -> Self {
66 RError::from_source(RErrorKind::Other, e)
67 }
68}
69
70fn resolved_path_string(interp: &Interpreter, path: &str) -> String {
73 interp.resolve_path(path).to_string_lossy().to_string()
74}
75
76fn home_dir_string(interp: &Interpreter) -> Option<String> {
77 interp
78 .get_env_var("HOME")
79 .or_else(|| interp.get_env_var("USERPROFILE"))
80 .or_else(|| {
81 #[cfg(feature = "dirs-support")]
82 {
83 dirs::home_dir().map(|home| home.to_string_lossy().to_string())
84 }
85 #[cfg(not(feature = "dirs-support"))]
86 {
87 None
88 }
89 })
90}
91
92fn minir_data_dir(interp: &Interpreter) -> String {
93 #[cfg(feature = "dirs-support")]
94 {
95 if let Some(data) = dirs::data_dir() {
96 return data.join("miniR").to_string_lossy().to_string();
97 }
98 }
99
100 home_dir_string(interp)
101 .map(|h| format!("{}/.miniR", h))
102 .unwrap_or_else(|| "/tmp/miniR".to_string())
103}
104
105#[interpreter_builtin(name = "file.copy", min_args = 2)]
113fn builtin_file_copy(
114 args: &[RValue],
115 _named: &[(String, RValue)],
116 context: &BuiltinContext,
117) -> Result<RValue, RError> {
118 let from = args
119 .first()
120 .and_then(|v| v.as_vector()?.as_character_scalar())
121 .ok_or_else(|| {
122 RError::new(
123 RErrorKind::Argument,
124 "'from' must be a character string".to_string(),
125 )
126 })?;
127 let to = args
128 .get(1)
129 .and_then(|v| v.as_vector()?.as_character_scalar())
130 .ok_or_else(|| {
131 RError::new(
132 RErrorKind::Argument,
133 "'to' must be a character string".to_string(),
134 )
135 })?;
136
137 let from = resolved_path_string(context.interpreter(), &from);
138 let to = resolved_path_string(context.interpreter(), &to);
139
140 match fs::copy(&from, &to) {
141 Ok(_) => Ok(RValue::vec(Vector::Logical(vec![Some(true)].into()))),
142 Err(source) => Err(SystemError::Copy { from, to, source }.into()),
143 }
144}
145
146#[interpreter_builtin(name = "file.create", min_args = 1)]
151fn builtin_file_create(
152 args: &[RValue],
153 _named: &[(String, RValue)],
154 context: &BuiltinContext,
155) -> Result<RValue, RError> {
156 let results: Vec<Option<bool>> = args
157 .iter()
158 .map(|arg| {
159 let path = arg
160 .as_vector()
161 .and_then(|v| v.as_character_scalar())
162 .unwrap_or_default();
163 let path = resolved_path_string(context.interpreter(), &path);
164 match fs::File::create(&path) {
165 Ok(_) => Some(true),
166 Err(_) => Some(false),
167 }
168 })
169 .collect();
170 Ok(RValue::vec(Vector::Logical(results.into())))
171}
172
173#[interpreter_builtin(name = "file.remove", min_args = 1)]
178fn builtin_file_remove(
179 args: &[RValue],
180 _named: &[(String, RValue)],
181 context: &BuiltinContext,
182) -> Result<RValue, RError> {
183 let results: Vec<Option<bool>> = args
184 .iter()
185 .map(|arg| {
186 let path = arg
187 .as_vector()
188 .and_then(|v| v.as_character_scalar())
189 .unwrap_or_default();
190 let path = resolved_path_string(context.interpreter(), &path);
191 match fs::remove_file(&path) {
192 Ok(()) => Some(true),
193 Err(_) => Some(false),
194 }
195 })
196 .collect();
197 Ok(RValue::vec(Vector::Logical(results.into())))
198}
199
200#[interpreter_builtin(name = "file.rename", min_args = 2)]
206fn builtin_file_rename(
207 args: &[RValue],
208 _named: &[(String, RValue)],
209 context: &BuiltinContext,
210) -> Result<RValue, RError> {
211 let from = args
212 .first()
213 .and_then(|v| v.as_vector()?.as_character_scalar())
214 .ok_or_else(|| {
215 RError::new(
216 RErrorKind::Argument,
217 "'from' must be a character string".to_string(),
218 )
219 })?;
220 let to = args
221 .get(1)
222 .and_then(|v| v.as_vector()?.as_character_scalar())
223 .ok_or_else(|| {
224 RError::new(
225 RErrorKind::Argument,
226 "'to' must be a character string".to_string(),
227 )
228 })?;
229
230 let from = resolved_path_string(context.interpreter(), &from);
231 let to = resolved_path_string(context.interpreter(), &to);
232
233 match fs::rename(&from, &to) {
234 Ok(()) => Ok(RValue::vec(Vector::Logical(vec![Some(true)].into()))),
235 Err(source) => Err(SystemError::Rename { from, to, source }.into()),
236 }
237}
238
239#[interpreter_builtin(name = "file.size", min_args = 1)]
244fn builtin_file_size(
245 args: &[RValue],
246 _named: &[(String, RValue)],
247 context: &BuiltinContext,
248) -> Result<RValue, RError> {
249 let results: Vec<Option<f64>> = args
250 .iter()
251 .map(|arg| {
252 let path = arg
253 .as_vector()
254 .and_then(|v| v.as_character_scalar())
255 .unwrap_or_default();
256 let path = resolved_path_string(context.interpreter(), &path);
257 fs::metadata(&path).ok().map(|m| u64_to_f64(m.len()))
258 })
259 .collect();
260 Ok(RValue::vec(Vector::Double(results.into())))
261}
262
263#[interpreter_builtin(name = "file.mtime", min_args = 1)]
268fn builtin_file_mtime(
269 args: &[RValue],
270 _named: &[(String, RValue)],
271 context: &BuiltinContext,
272) -> Result<RValue, RError> {
273 let results: Vec<Option<f64>> = args
274 .iter()
275 .map(|arg| {
276 let path = arg
277 .as_vector()
278 .and_then(|v| v.as_character_scalar())
279 .unwrap_or_default();
280 let path = resolved_path_string(context.interpreter(), &path);
281 fs::metadata(&path)
282 .ok()
283 .and_then(|m| m.modified().ok())
284 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
285 .map(|d| d.as_secs_f64())
286 })
287 .collect();
288 let mut rv = RVector::from(Vector::Double(results.into()));
289 rv.set_attr(
290 "class".to_string(),
291 RValue::vec(Vector::Character(
292 vec![Some("POSIXct".to_string()), Some("POSIXt".to_string())].into(),
293 )),
294 );
295 Ok(RValue::Vector(rv))
296}
297
298#[interpreter_builtin(name = "unlink", min_args = 1)]
304fn builtin_unlink(
305 args: &[RValue],
306 named: &[(String, RValue)],
307 context: &BuiltinContext,
308) -> Result<RValue, RError> {
309 let path = args
310 .first()
311 .and_then(|v| v.as_vector()?.as_character_scalar())
312 .ok_or_else(|| {
313 RError::new(
314 RErrorKind::Argument,
315 "'x' must be a character string".to_string(),
316 )
317 })?;
318 let recursive = named
319 .iter()
320 .find(|(n, _)| n == "recursive")
321 .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
322 .unwrap_or(false);
323
324 let path = resolved_path_string(context.interpreter(), &path);
325 let p = Path::new(&path);
326 let result = if p.is_dir() {
327 if recursive {
328 fs::remove_dir_all(&path)
329 } else {
330 fs::remove_dir(&path)
331 }
332 } else {
333 fs::remove_file(&path)
334 };
335
336 match result {
337 Ok(()) => Ok(RValue::vec(Vector::Integer(vec![Some(0)].into()))),
338 Err(_) => Ok(RValue::vec(Vector::Integer(vec![Some(1)].into()))),
339 }
340}
341
342#[interpreter_builtin(name = "file.info", min_args = 1)]
349fn builtin_file_info(
350 args: &[RValue],
351 _named: &[(String, RValue)],
352 context: &BuiltinContext,
353) -> Result<RValue, RError> {
354 let paths: Vec<String> = args
355 .iter()
356 .filter_map(|v| v.as_vector()?.as_character_scalar())
357 .map(|path| resolved_path_string(context.interpreter(), &path))
358 .collect();
359
360 if paths.is_empty() {
361 return Err(RError::new(
362 RErrorKind::Argument,
363 "'...' must contain at least one file path".to_string(),
364 ));
365 }
366
367 let mut sizes: Vec<Option<f64>> = Vec::new();
368 let mut isdirs: Vec<Option<bool>> = Vec::new();
369 let mut modes: Vec<Option<i64>> = Vec::new();
370 let mut mtimes: Vec<Option<f64>> = Vec::new();
371 let mut ctimes: Vec<Option<f64>> = Vec::new();
372 let mut atimes: Vec<Option<f64>> = Vec::new();
373
374 for path in &paths {
375 match fs::metadata(path) {
376 Ok(meta) => {
377 sizes.push(Some(u64_to_f64(meta.len())));
378 isdirs.push(Some(meta.is_dir()));
379
380 #[cfg(unix)]
381 {
382 use std::os::unix::fs::PermissionsExt;
383 let mode_u32 = meta.permissions().mode() & 0o777;
384 modes.push(Some(i64::from(mode_u32)));
385 }
386 #[cfg(not(unix))]
387 {
388 modes.push(Some(0o644));
389 }
390
391 let mtime = meta
392 .modified()
393 .ok()
394 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
395 .map(|d| d.as_secs_f64());
396 mtimes.push(mtime);
397
398 let ctime = meta
399 .created()
400 .ok()
401 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
402 .map(|d| d.as_secs_f64());
403 ctimes.push(ctime);
404
405 let atime = meta
406 .accessed()
407 .ok()
408 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
409 .map(|d| d.as_secs_f64());
410 atimes.push(atime);
411 }
412 Err(_) => {
413 sizes.push(None);
414 isdirs.push(None);
415 modes.push(None);
416 mtimes.push(None);
417 ctimes.push(None);
418 atimes.push(None);
419 }
420 }
421 }
422
423 let row_names: Vec<Option<String>> = paths.into_iter().map(Some).collect();
424 let mut list = RList::new(vec![
425 (
426 Some("size".to_string()),
427 RValue::vec(Vector::Double(sizes.into())),
428 ),
429 (
430 Some("isdir".to_string()),
431 RValue::vec(Vector::Logical(isdirs.into())),
432 ),
433 (
434 Some("mode".to_string()),
435 RValue::vec(Vector::Integer(modes.into())),
436 ),
437 (
438 Some("mtime".to_string()),
439 RValue::vec(Vector::Double(mtimes.into())),
440 ),
441 (
442 Some("ctime".to_string()),
443 RValue::vec(Vector::Double(ctimes.into())),
444 ),
445 (
446 Some("atime".to_string()),
447 RValue::vec(Vector::Double(atimes.into())),
448 ),
449 ]);
450
451 list.set_attr(
452 "row.names".to_string(),
453 RValue::vec(Vector::Character(row_names.into())),
454 );
455
456 Ok(RValue::List(list))
457}
458#[interpreter_builtin(name = "dir.create", min_args = 1)]
470fn builtin_dir_create(
471 args: &[RValue],
472 named: &[(String, RValue)],
473 context: &BuiltinContext,
474) -> Result<RValue, RError> {
475 let path = args
476 .first()
477 .and_then(|v| v.as_vector()?.as_character_scalar())
478 .ok_or_else(|| {
479 RError::new(
480 RErrorKind::Argument,
481 "'path' must be a character string".to_string(),
482 )
483 })?;
484
485 let show_warnings = named
486 .iter()
487 .find(|(n, _)| n == "showWarnings")
488 .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
489 .unwrap_or(true);
490
491 let recursive = named
493 .iter()
494 .find(|(n, _)| n == "recursive")
495 .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
496 .unwrap_or(true);
497
498 let path = resolved_path_string(context.interpreter(), &path);
499
500 let result = if recursive {
501 fs::create_dir_all(&path)
502 } else {
503 fs::create_dir(&path)
504 };
505
506 match result {
507 Ok(()) => Ok(RValue::vec(Vector::Logical(vec![Some(true)].into()))),
508 Err(source) => {
509 if show_warnings {
510 Ok(RValue::vec(Vector::Logical(vec![Some(false)].into())))
514 } else {
515 Err(SystemError::CreateDir { path, source }.into())
516 }
517 }
518 }
519}
520
521#[interpreter_builtin(name = "dir.exists", min_args = 1)]
526fn builtin_dir_exists(
527 args: &[RValue],
528 _named: &[(String, RValue)],
529 context: &BuiltinContext,
530) -> Result<RValue, RError> {
531 let results: Vec<Option<bool>> = args
532 .iter()
533 .map(|arg| {
534 let path = arg
535 .as_vector()
536 .and_then(|v| v.as_character_scalar())
537 .unwrap_or_default();
538 let path = resolved_path_string(context.interpreter(), &path);
539 Some(Path::new(&path).is_dir())
540 })
541 .collect();
542 Ok(RValue::vec(Vector::Logical(results.into())))
543}
544
545#[interpreter_builtin(name = "list.files", names = ["dir"])]
556fn builtin_list_files(
557 args: &[RValue],
558 named: &[(String, RValue)],
559 context: &BuiltinContext,
560) -> Result<RValue, RError> {
561 let path = args
562 .first()
563 .and_then(|v| v.as_vector()?.as_character_scalar())
564 .unwrap_or_else(|| ".".to_string());
565 let path = resolved_path_string(context.interpreter(), &path);
566
567 let pattern = named
568 .iter()
569 .find(|(n, _)| n == "pattern")
570 .and_then(|(_, v)| v.as_vector()?.as_character_scalar());
571
572 let all_files = named
573 .iter()
574 .find(|(n, _)| n == "all.files")
575 .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
576 .unwrap_or(false);
577
578 let recursive = named
579 .iter()
580 .find(|(n, _)| n == "recursive")
581 .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
582 .unwrap_or(false);
583
584 let full_names = named
585 .iter()
586 .find(|(n, _)| n == "full.names")
587 .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
588 .unwrap_or(false);
589
590 let regex = match &pattern {
591 Some(pat) => Some(regex::Regex::new(pat).map_err(|source| -> RError {
592 super::strings::StringError::InvalidRegex { source }.into()
593 })?),
594 None => None,
595 };
596
597 let result = if recursive {
598 list_files_recursive(&path, ®ex, all_files, full_names)?
599 } else {
600 list_files_flat(&path, ®ex, all_files, full_names)?
601 };
602
603 Ok(RValue::vec(Vector::Character(result.into())))
604}
605
606fn list_files_flat(
608 path: &str,
609 regex: &Option<regex::Regex>,
610 all_files: bool,
611 full_names: bool,
612) -> Result<Vec<Option<String>>, RError> {
613 let entries = fs::read_dir(path).map_err(|source| SystemError::ReadDir {
614 path: path.to_string(),
615 source,
616 })?;
617
618 let result: Vec<Option<String>> = entries
619 .filter_map(|entry| {
620 let entry = entry.ok()?;
621 let name = entry.file_name().into_string().ok()?;
622 if !all_files && name.starts_with('.') {
624 return None;
625 }
626 if let Some(ref re) = regex {
627 if !re.is_match(&name) {
628 return None;
629 }
630 }
631 if full_names {
632 Some(entry.path().to_string_lossy().to_string())
633 } else {
634 Some(name)
635 }
636 })
637 .sorted()
638 .map(Some)
639 .collect();
640 Ok(result)
641}
642
643#[cfg(feature = "walkdir-support")]
645fn list_files_recursive(
646 path: &str,
647 regex: &Option<regex::Regex>,
648 all_files: bool,
649 full_names: bool,
650) -> Result<Vec<Option<String>>, RError> {
651 let base = Path::new(path);
652 let result: Vec<Option<String>> = walkdir::WalkDir::new(path)
653 .min_depth(1)
654 .into_iter()
655 .filter_map(|entry| {
656 let entry = entry.ok()?;
657 let name = entry.file_name().to_string_lossy().to_string();
658 if !all_files && name.starts_with('.') {
660 return None;
661 }
662 if let Some(ref re) = regex {
663 if !re.is_match(&name) {
664 return None;
665 }
666 }
667 if full_names {
668 Some(entry.path().to_string_lossy().to_string())
669 } else {
670 entry
672 .path()
673 .strip_prefix(base)
674 .ok()
675 .map(|p| p.to_string_lossy().to_string())
676 }
677 })
678 .sorted()
679 .map(Some)
680 .collect();
681 Ok(result)
682}
683
684#[cfg(not(feature = "walkdir-support"))]
686fn list_files_recursive(
687 path: &str,
688 regex: &Option<regex::Regex>,
689 all_files: bool,
690 full_names: bool,
691) -> Result<Vec<Option<String>>, RError> {
692 let mut result: Vec<String> = Vec::new();
693 list_files_recursive_fallback(
694 Path::new(path),
695 Path::new(path),
696 regex,
697 all_files,
698 full_names,
699 &mut result,
700 )?;
701 result.sort();
702 Ok(result.into_iter().map(Some).collect())
703}
704
705#[cfg(not(feature = "walkdir-support"))]
706fn list_files_recursive_fallback(
707 base: &Path,
708 dir: &Path,
709 regex: &Option<regex::Regex>,
710 all_files: bool,
711 full_names: bool,
712 out: &mut Vec<String>,
713) -> Result<(), RError> {
714 let entries = fs::read_dir(dir).map_err(|source| SystemError::ReadDir {
715 path: dir.to_string_lossy().to_string(),
716 source,
717 })?;
718 for entry in entries {
719 let entry = match entry {
720 Ok(e) => e,
721 Err(_) => continue,
722 };
723 let name = entry.file_name().to_string_lossy().to_string();
724 let entry_path = entry.path();
725
726 if !all_files && name.starts_with('.') {
728 continue;
729 }
730
731 if entry_path.is_dir() {
732 list_files_recursive_fallback(base, &entry_path, regex, all_files, full_names, out)?;
733 } else {
734 if let Some(ref re) = regex {
735 if !re.is_match(&name) {
736 continue;
737 }
738 }
739 if full_names {
740 out.push(entry_path.to_string_lossy().to_string());
741 } else if let Ok(rel) = entry_path.strip_prefix(base) {
742 out.push(rel.to_string_lossy().to_string());
743 }
744 }
745 }
746 Ok(())
747}
748
749#[interpreter_builtin(name = "list.dirs")]
760fn builtin_list_dirs(
761 args: &[RValue],
762 named: &[(String, RValue)],
763 context: &BuiltinContext,
764) -> Result<RValue, RError> {
765 let path = args
766 .first()
767 .and_then(|v| v.as_vector()?.as_character_scalar())
768 .unwrap_or_else(|| ".".to_string());
769 let path = resolved_path_string(context.interpreter(), &path);
770
771 let full_names = named
773 .iter()
774 .find(|(n, _)| n == "full.names")
775 .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
776 .unwrap_or(true);
777
778 let recursive = named
779 .iter()
780 .find(|(n, _)| n == "recursive")
781 .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
782 .unwrap_or(true);
783
784 let result = if recursive {
785 list_dirs_recursive(&path, full_names)?
786 } else {
787 list_dirs_flat(&path, full_names)?
788 };
789
790 Ok(RValue::vec(Vector::Character(result.into())))
791}
792
793fn list_dirs_flat(path: &str, full_names: bool) -> Result<Vec<Option<String>>, RError> {
795 let base = Path::new(path);
796 let entries = fs::read_dir(path).map_err(|source| SystemError::ReadDir {
797 path: path.to_string(),
798 source,
799 })?;
800
801 let mut result: Vec<String> = vec![if full_names {
803 base.to_string_lossy().to_string()
804 } else {
805 ".".to_string()
806 }];
807
808 for entry in entries {
809 let entry = match entry {
810 Ok(e) => e,
811 Err(_) => continue,
812 };
813 let entry_path = entry.path();
814 if !entry_path.is_dir() {
815 continue;
816 }
817 let name = entry.file_name().into_string().unwrap_or_default();
818 if full_names {
819 result.push(entry_path.to_string_lossy().to_string());
820 } else {
821 result.push(name);
822 }
823 }
824
825 result.sort();
826 Ok(result.into_iter().map(Some).collect())
827}
828
829#[cfg(feature = "walkdir-support")]
831fn list_dirs_recursive(path: &str, full_names: bool) -> Result<Vec<Option<String>>, RError> {
832 let base = Path::new(path);
833 let mut result: Vec<String> = walkdir::WalkDir::new(path)
834 .into_iter()
835 .filter_map(|entry| {
836 let entry = entry.ok()?;
837 if !entry.file_type().is_dir() {
838 return None;
839 }
840 if full_names {
841 Some(entry.path().to_string_lossy().to_string())
842 } else {
843 let rel = entry.path().strip_prefix(base).ok()?;
844 let s = rel.to_string_lossy().to_string();
845 Some(if s.is_empty() { ".".to_string() } else { s })
846 }
847 })
848 .collect();
849 result.sort();
850 Ok(result.into_iter().map(Some).collect())
851}
852
853#[cfg(not(feature = "walkdir-support"))]
855fn list_dirs_recursive(path: &str, full_names: bool) -> Result<Vec<Option<String>>, RError> {
856 let base = Path::new(path);
857 let mut result: Vec<String> = Vec::new();
858 list_dirs_recursive_fallback(base, base, full_names, &mut result)?;
859 result.sort();
860 Ok(result.into_iter().map(Some).collect())
861}
862
863#[cfg(not(feature = "walkdir-support"))]
864fn list_dirs_recursive_fallback(
865 base: &Path,
866 dir: &Path,
867 full_names: bool,
868 out: &mut Vec<String>,
869) -> Result<(), RError> {
870 if full_names {
872 out.push(dir.to_string_lossy().to_string());
873 } else {
874 let rel = dir
875 .strip_prefix(base)
876 .ok()
877 .map(|p| p.to_string_lossy().to_string())
878 .unwrap_or_default();
879 out.push(if rel.is_empty() { ".".to_string() } else { rel });
880 }
881
882 let entries = fs::read_dir(dir).map_err(|source| SystemError::ReadDir {
883 path: dir.to_string_lossy().to_string(),
884 source,
885 })?;
886 for entry in entries {
887 let entry = match entry {
888 Ok(e) => e,
889 Err(_) => continue,
890 };
891 let entry_path = entry.path();
892 if entry_path.is_dir() {
893 list_dirs_recursive_fallback(base, &entry_path, full_names, out)?;
894 }
895 }
896 Ok(())
897}
898
899#[interpreter_builtin]
907fn interp_tempdir(
908 _args: &[RValue],
909 _named: &[(String, RValue)],
910 context: &BuiltinContext,
911) -> Result<RValue, RError> {
912 let path =
913 context.with_interpreter(|interp| interp.temp_dir.path().to_string_lossy().to_string());
914 Ok(RValue::vec(Vector::Character(vec![Some(path)].into())))
915}
916
917#[interpreter_builtin]
924fn interp_tempfile(
925 args: &[RValue],
926 named: &[(String, RValue)],
927 context: &BuiltinContext,
928) -> Result<RValue, RError> {
929 let pattern = args
930 .first()
931 .or_else(|| named.iter().find(|(n, _)| n == "pattern").map(|(_, v)| v))
932 .and_then(|v| v.as_vector()?.as_character_scalar())
933 .unwrap_or_else(|| "file".to_string());
934
935 let fileext = args
936 .get(2)
937 .or_else(|| named.iter().find(|(n, _)| n == "fileext").map(|(_, v)| v))
938 .and_then(|v| v.as_vector()?.as_character_scalar())
939 .unwrap_or_default();
940
941 let path = context.with_interpreter(|interp| {
942 let tmpdir = args
943 .get(1)
944 .or_else(|| named.iter().find(|(n, _)| n == "tmpdir").map(|(_, v)| v))
945 .and_then(|v| v.as_vector()?.as_character_scalar())
946 .unwrap_or_else(|| interp.temp_dir.path().to_string_lossy().to_string());
947
948 let n = interp.temp_counter.get();
949 interp.temp_counter.set(n + 1);
950
951 Path::new(&tmpdir)
952 .join(format!("{}{}{}", pattern, n, fileext))
953 .to_string_lossy()
954 .to_string()
955 });
956 Ok(RValue::vec(Vector::Character(vec![Some(path)].into())))
957}
958
959#[interpreter_builtin(name = "Sys.glob", min_args = 1)]
971fn builtin_sys_glob(
972 args: &[RValue],
973 _named: &[(String, RValue)],
974 context: &BuiltinContext,
975) -> Result<RValue, RError> {
976 let patterns: Vec<String> = args
977 .iter()
978 .filter_map(|v| v.as_vector()?.as_character_scalar())
979 .collect();
980
981 let mut results: Vec<Option<String>> = Vec::new();
982 for pattern in &patterns {
983 let resolved_pattern = resolved_path_string(context.interpreter(), pattern);
984 #[cfg(feature = "globset-support")]
986 {
987 if let Err(e) = globset::Glob::new(&resolved_pattern) {
988 return Err(RError::other(format!(
989 "invalid glob pattern '{}': {}",
990 pattern, e
991 )));
992 }
993 }
994
995 match glob::glob(&resolved_pattern) {
996 Ok(paths) => {
997 for path in paths.flatten() {
998 results.push(Some(path.to_string_lossy().to_string()));
999 }
1000 }
1001 Err(e) => {
1002 return Err(RError::other(format!(
1003 "invalid glob pattern '{}': {}",
1004 pattern, e
1005 )));
1006 }
1007 }
1008 }
1009
1010 Ok(RValue::vec(Vector::Character(results.into())))
1011}
1012
1013#[interpreter_builtin(name = "normalizePath", min_args = 1)]
1022fn builtin_normalize_path(
1023 args: &[RValue],
1024 named: &[(String, RValue)],
1025 context: &BuiltinContext,
1026) -> Result<RValue, RError> {
1027 let path = args
1028 .first()
1029 .and_then(|v| v.as_vector()?.as_character_scalar())
1030 .ok_or_else(|| {
1031 RError::new(
1032 RErrorKind::Argument,
1033 "'path' must be a character string".to_string(),
1034 )
1035 })?;
1036
1037 let must_work = named
1038 .iter()
1039 .find(|(n, _)| n == "mustWork")
1040 .or_else(|| named.iter().find(|(n, _)| n == "mustwork"))
1041 .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
1042 .or_else(|| args.get(2).and_then(|v| v.as_vector()?.as_logical_scalar()))
1043 .unwrap_or(false);
1044
1045 let resolved = context.interpreter().resolve_path(&path);
1046
1047 match fs::canonicalize(&resolved) {
1048 Ok(p) => Ok(RValue::vec(Vector::Character(
1049 vec![Some(p.to_string_lossy().to_string())].into(),
1050 ))),
1051 Err(e) => {
1052 if must_work {
1053 Err(RError::new(
1054 RErrorKind::Other,
1055 format!("path '{}' does not exist: {}", path, e),
1056 ))
1057 } else {
1058 Ok(RValue::vec(Vector::Character(
1059 vec![Some(path.clone())].into(),
1060 )))
1061 }
1062 }
1063 }
1064}
1065
1066#[interpreter_builtin(name = "path.expand", min_args = 1)]
1075fn builtin_path_expand(
1076 args: &[RValue],
1077 _named: &[(String, RValue)],
1078 context: &BuiltinContext,
1079) -> Result<RValue, RError> {
1080 let path = args
1081 .first()
1082 .and_then(|v| v.as_vector()?.as_character_scalar())
1083 .ok_or_else(|| {
1084 RError::new(
1085 RErrorKind::Argument,
1086 "'path' must be a character string".to_string(),
1087 )
1088 })?;
1089
1090 let expanded = if path.starts_with('~') {
1091 let home = home_dir_string(context.interpreter());
1092 match home {
1093 Some(h) => path.replacen('~', &h, 1),
1094 None => path,
1095 }
1096 } else {
1097 path
1098 };
1099
1100 Ok(RValue::vec(Vector::Character(vec![Some(expanded)].into())))
1101}
1102
1103#[interpreter_builtin(name = "R.home")]
1114fn builtin_r_home(
1115 args: &[RValue],
1116 _named: &[(String, RValue)],
1117 context: &BuiltinContext,
1118) -> Result<RValue, RError> {
1119 let component = args
1120 .first()
1121 .and_then(|v| v.as_vector()?.as_character_scalar())
1122 .unwrap_or_default();
1123
1124 let base = minir_data_dir(context.interpreter());
1125 let result = if component.is_empty() {
1126 base
1127 } else {
1128 Path::new(&base)
1129 .join(&component)
1130 .to_string_lossy()
1131 .to_string()
1132 };
1133
1134 Ok(RValue::vec(Vector::Character(vec![Some(result)].into())))
1135}
1136
1137#[interpreter_builtin(name = ".libPaths")]
1150fn builtin_lib_paths(
1151 _args: &[RValue],
1152 _named: &[(String, RValue)],
1153 context: &BuiltinContext,
1154) -> Result<RValue, RError> {
1155 let interp = context.interpreter();
1156 let mut paths: Vec<String> = Vec::new();
1157
1158 let sep = if cfg!(windows) { ';' } else { ':' };
1160
1161 if let Some(r_libs) = interp.get_env_var("R_LIBS") {
1163 for p in r_libs.split(sep) {
1164 let p = p.trim();
1165 if !p.is_empty() {
1166 let resolved = interp.resolve_path(p);
1167 if resolved.is_dir() {
1168 paths.push(resolved.to_string_lossy().to_string());
1169 }
1170 }
1171 }
1172 }
1173
1174 if let Some(r_libs_user) = interp.get_env_var("R_LIBS_USER") {
1176 for p in r_libs_user.split(sep) {
1177 let p = p.trim();
1178 if !p.is_empty() {
1179 let resolved = interp.resolve_path(p);
1180 if resolved.is_dir() {
1181 paths.push(resolved.to_string_lossy().to_string());
1182 }
1183 }
1184 }
1185 }
1186
1187 let default_lib = format!("{}/library", minir_data_dir(interp));
1190 if !paths.contains(&default_lib) {
1191 paths.push(default_lib);
1192 }
1193
1194 let values: Vec<Option<String>> = paths.into_iter().map(Some).collect();
1195 Ok(RValue::vec(Vector::Character(values.into())))
1196}
1197
1198#[interpreter_builtin(name = "system", min_args = 1)]
1212fn builtin_system(
1213 args: &[RValue],
1214 named: &[(String, RValue)],
1215 context: &BuiltinContext,
1216) -> Result<RValue, RError> {
1217 let command = args
1218 .first()
1219 .and_then(|v| v.as_vector()?.as_character_scalar())
1220 .ok_or_else(|| {
1221 RError::new(
1222 RErrorKind::Argument,
1223 "'command' must be a character string".to_string(),
1224 )
1225 })?;
1226
1227 let intern = named
1228 .iter()
1229 .find(|(n, _)| n == "intern")
1230 .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
1231 .or_else(|| args.get(1).and_then(|v| v.as_vector()?.as_logical_scalar()))
1232 .unwrap_or(false);
1233
1234 if intern {
1235 let output = context.with_interpreter(|interp| {
1237 let mut cmd = std::process::Command::new("sh");
1238 cmd.arg("-c")
1239 .arg(&command)
1240 .current_dir(interp.get_working_dir())
1241 .env_clear()
1242 .envs(interp.env_vars_snapshot());
1243 cmd.output().map_err(|source| SystemError::Command {
1244 command: command.clone(),
1245 source,
1246 })
1247 })?;
1248
1249 let stdout = String::from_utf8_lossy(&output.stdout);
1250 let lines: Vec<Option<String>> =
1251 stdout.lines().map(|line| Some(line.to_string())).collect();
1252 Ok(RValue::vec(Vector::Character(lines.into())))
1253 } else {
1254 let output = context.with_interpreter(|interp| {
1256 let mut cmd = std::process::Command::new("sh");
1257 cmd.arg("-c")
1258 .arg(&command)
1259 .current_dir(interp.get_working_dir())
1260 .env_clear()
1261 .envs(interp.env_vars_snapshot());
1262 cmd.status().map_err(|source| SystemError::Command {
1263 command: command.clone(),
1264 source,
1265 })
1266 })?;
1267
1268 let code = i64::from(output.code().unwrap_or(-1));
1269 Ok(RValue::vec(Vector::Integer(vec![Some(code)].into())))
1270 }
1271}
1272
1273#[interpreter_builtin(name = "system2", min_args = 1)]
1284fn builtin_system2(
1285 args: &[RValue],
1286 named: &[(String, RValue)],
1287 context: &BuiltinContext,
1288) -> Result<RValue, RError> {
1289 let command = args
1290 .first()
1291 .and_then(|v| v.as_vector()?.as_character_scalar())
1292 .ok_or_else(|| {
1293 RError::new(
1294 RErrorKind::Argument,
1295 "'command' must be a character string".to_string(),
1296 )
1297 })?;
1298
1299 let cmd_args: Vec<String> = args
1300 .get(1)
1301 .or_else(|| named.iter().find(|(n, _)| n == "args").map(|(_, v)| v))
1302 .and_then(|v| v.as_vector())
1303 .map(|v| v.to_characters().into_iter().flatten().collect())
1304 .unwrap_or_default();
1305
1306 let stdout_val = named.iter().find(|(n, _)| n == "stdout").map(|(_, v)| v);
1308 let capture_stdout = stdout_val
1309 .and_then(|v| v.as_vector()?.as_logical_scalar())
1310 .unwrap_or(false);
1311
1312 let stderr_val = named.iter().find(|(n, _)| n == "stderr").map(|(_, v)| v);
1314 let capture_stderr = stderr_val
1315 .and_then(|v| v.as_vector()?.as_logical_scalar())
1316 .unwrap_or(false);
1317
1318 if capture_stdout || capture_stderr {
1319 let output = context.with_interpreter(|interp| {
1321 let mut cmd = std::process::Command::new(&command);
1322 cmd.args(&cmd_args)
1323 .current_dir(interp.get_working_dir())
1324 .env_clear()
1325 .envs(interp.env_vars_snapshot());
1326
1327 if capture_stdout {
1328 cmd.stdout(std::process::Stdio::piped());
1329 }
1330 if capture_stderr {
1331 cmd.stderr(std::process::Stdio::piped());
1332 }
1333
1334 cmd.output().map_err(|source| SystemError::Command {
1335 command: command.clone(),
1336 source,
1337 })
1338 })?;
1339
1340 let code = i64::from(output.status.code().unwrap_or(-1));
1341
1342 let mut lines: Vec<Option<String>> = Vec::new();
1344
1345 if capture_stdout {
1346 let stdout = String::from_utf8_lossy(&output.stdout);
1347 for line in stdout.lines() {
1348 lines.push(Some(line.to_string()));
1349 }
1350 }
1351
1352 if capture_stderr {
1353 let stderr = String::from_utf8_lossy(&output.stderr);
1354 for line in stderr.lines() {
1355 lines.push(Some(line.to_string()));
1356 }
1357 }
1358
1359 let mut rv = RVector::from(Vector::Character(lines.into()));
1360 rv.set_attr(
1361 "status".to_string(),
1362 RValue::vec(Vector::Integer(vec![Some(code)].into())),
1363 );
1364 Ok(RValue::Vector(rv))
1365 } else {
1366 let output = context.with_interpreter(|interp| {
1368 let mut cmd = std::process::Command::new(&command);
1369 cmd.args(&cmd_args)
1370 .current_dir(interp.get_working_dir())
1371 .env_clear()
1372 .envs(interp.env_vars_snapshot());
1373 cmd.status().map_err(|source| SystemError::Command {
1374 command: command.clone(),
1375 source,
1376 })
1377 })?;
1378
1379 let code = i64::from(output.code().unwrap_or(-1));
1380 Ok(RValue::vec(Vector::Integer(vec![Some(code)].into())))
1381 }
1382}
1383
1384#[interpreter_builtin(name = "Sys.setenv")]
1389fn interp_sys_setenv(
1390 _args: &[RValue],
1391 named: &[(String, RValue)],
1392 context: &BuiltinContext,
1393) -> Result<RValue, RError> {
1394 context.with_interpreter(|interp| {
1395 for (name, val) in named {
1396 let val_str = val
1397 .as_vector()
1398 .and_then(|v| v.as_character_scalar())
1399 .unwrap_or_default();
1400 interp.set_env_var(name.clone(), val_str);
1401 }
1402 });
1403 Ok(RValue::vec(Vector::Logical(vec![Some(true)].into())))
1404}
1405
1406#[interpreter_builtin(name = "Sys.unsetenv", min_args = 1)]
1411fn interp_sys_unsetenv(
1412 args: &[RValue],
1413 _named: &[(String, RValue)],
1414 context: &BuiltinContext,
1415) -> Result<RValue, RError> {
1416 let names: Vec<Option<String>> = args
1417 .first()
1418 .and_then(|v| v.as_vector())
1419 .map(|v| v.to_characters())
1420 .unwrap_or_default();
1421
1422 let results: Vec<Option<bool>> = names
1423 .iter()
1424 .map(|n| {
1425 if let Some(name) = n {
1426 context.with_interpreter(|interp| interp.remove_env_var(name));
1427 Some(true)
1428 } else {
1429 Some(false)
1430 }
1431 })
1432 .collect();
1433 Ok(RValue::vec(Vector::Logical(results.into())))
1434}
1435
1436#[interpreter_builtin(name = "Sys.which", min_args = 1)]
1442fn interp_sys_which(
1443 args: &[RValue],
1444 _named: &[(String, RValue)],
1445 context: &BuiltinContext,
1446) -> Result<RValue, RError> {
1447 let names: Vec<Option<String>> = args
1449 .first()
1450 .and_then(|v| v.as_vector())
1451 .map(|v| v.to_characters())
1452 .unwrap_or_default();
1453
1454 let path_var = context
1455 .with_interpreter(|interp| interp.get_env_var("PATH"))
1456 .unwrap_or_default();
1457 let sep = if cfg!(windows) { ';' } else { ':' };
1458 let path_dirs: Vec<&str> = path_var.split(sep).collect();
1459
1460 let results: Vec<Option<String>> = names
1461 .iter()
1462 .map(|name_opt| {
1463 let name = match name_opt {
1464 Some(n) => n,
1465 None => return Some(String::new()),
1466 };
1467 if name.contains('/') || (cfg!(windows) && name.contains('\\')) {
1469 let p = Path::new(name);
1470 if p.is_file() {
1471 return Some(p.to_string_lossy().to_string());
1472 }
1473 return Some(String::new());
1474 }
1475 for dir in &path_dirs {
1476 let candidate = Path::new(dir).join(name);
1477 if candidate.is_file() {
1478 #[cfg(unix)]
1479 {
1480 use std::os::unix::fs::PermissionsExt;
1481 if let Ok(meta) = candidate.metadata() {
1482 if meta.permissions().mode() & 0o111 != 0 {
1483 return Some(candidate.to_string_lossy().to_string());
1484 }
1485 }
1486 continue;
1487 }
1488 #[cfg(not(unix))]
1489 {
1490 return Some(candidate.to_string_lossy().to_string());
1491 }
1492 }
1493 }
1494 Some(String::new())
1495 })
1496 .collect();
1497
1498 let mut rv = RVector::from(Vector::Character(results.into()));
1500 rv.set_attr(
1501 "names".to_string(),
1502 RValue::vec(Vector::Character(names.into())),
1503 );
1504 Ok(RValue::Vector(rv))
1505}
1506
1507#[interpreter_builtin(min_args = 1)]
1512fn interp_setwd(
1513 args: &[RValue],
1514 _named: &[(String, RValue)],
1515 context: &BuiltinContext,
1516) -> Result<RValue, RError> {
1517 let dir = args
1518 .first()
1519 .and_then(|v| v.as_vector()?.as_character_scalar())
1520 .ok_or_else(|| {
1521 RError::new(
1522 RErrorKind::Argument,
1523 "'dir' must be a character string".to_string(),
1524 )
1525 })?;
1526
1527 let path = Path::new(&dir);
1528 if !path.is_dir() {
1529 return Err(SystemError::SetCwd {
1530 path: dir.clone(),
1531 source: std::io::Error::new(std::io::ErrorKind::NotFound, "no such directory"),
1532 }
1533 .into());
1534 }
1535
1536 context.with_interpreter(|interp| {
1537 let old_wd = interp.get_working_dir().to_string_lossy().to_string();
1538 interp.set_working_dir(path.to_path_buf());
1539 Ok(RValue::vec(Vector::Character(vec![Some(old_wd)].into())))
1540 })
1541}
1542
1543#[interpreter_builtin(name = "Sys.sleep", min_args = 1)]
1550fn interp_sys_sleep(
1551 args: &[RValue],
1552 _named: &[(String, RValue)],
1553 context: &BuiltinContext,
1554) -> Result<RValue, RError> {
1555 let time = args
1556 .first()
1557 .and_then(|v| v.as_vector()?.as_double_scalar())
1558 .ok_or_else(|| {
1559 RError::new(
1560 RErrorKind::Argument,
1561 "'time' must be a numeric value".to_string(),
1562 )
1563 })?;
1564
1565 if time > 0.0 {
1566 std::thread::sleep(std::time::Duration::from_secs_f64(time));
1567 }
1568
1569 context.interpreter().set_invisible();
1570 Ok(RValue::Null)
1571}
1572
1573#[interpreter_builtin(name = "Sys.info")]
1582fn builtin_sys_info(
1583 _args: &[RValue],
1584 _named: &[(String, RValue)],
1585 context: &BuiltinContext,
1586) -> Result<RValue, RError> {
1587 let sysname = if cfg!(target_os = "macos") {
1588 "Darwin"
1589 } else if cfg!(target_os = "linux") {
1590 "Linux"
1591 } else if cfg!(target_os = "windows") {
1592 "Windows"
1593 } else {
1594 "Unknown"
1595 };
1596
1597 let machine = if cfg!(target_arch = "x86_64") {
1598 "x86_64"
1599 } else if cfg!(target_arch = "aarch64") {
1600 "aarch64"
1601 } else {
1602 "unknown"
1603 };
1604
1605 let nodename = std::process::Command::new("hostname")
1606 .output()
1607 .ok()
1608 .and_then(|o| String::from_utf8(o.stdout).ok())
1609 .map(|s| s.trim().to_string())
1610 .unwrap_or_else(|| "unknown".to_string());
1611
1612 let release = std::process::Command::new("uname")
1614 .arg("-r")
1615 .output()
1616 .ok()
1617 .and_then(|o| String::from_utf8(o.stdout).ok())
1618 .map(|s| s.trim().to_string())
1619 .unwrap_or_else(|| "unknown".to_string());
1620
1621 let version = std::process::Command::new("uname")
1622 .arg("-v")
1623 .output()
1624 .ok()
1625 .and_then(|o| String::from_utf8(o.stdout).ok())
1626 .map(|s| s.trim().to_string())
1627 .unwrap_or_else(|| "unknown".to_string());
1628
1629 let user = context
1630 .with_interpreter(|interp| {
1631 interp
1632 .get_env_var("USER")
1633 .or_else(|| interp.get_env_var("USERNAME"))
1634 })
1635 .unwrap_or_else(|| "unknown".to_string());
1636
1637 let field_names = vec![
1639 Some("sysname".to_string()),
1640 Some("nodename".to_string()),
1641 Some("release".to_string()),
1642 Some("version".to_string()),
1643 Some("machine".to_string()),
1644 Some("login".to_string()),
1645 Some("user".to_string()),
1646 ];
1647 let field_values = vec![
1648 Some(sysname.to_string()),
1649 Some(nodename),
1650 Some(release),
1651 Some(version),
1652 Some(machine.to_string()),
1653 Some(user.clone()),
1654 Some(user),
1655 ];
1656
1657 let mut rv = RVector::from(Vector::Character(field_values.into()));
1658 rv.set_attr(
1659 "names".to_string(),
1660 RValue::vec(Vector::Character(field_names.into())),
1661 );
1662 Ok(RValue::Vector(rv))
1663}
1664
1665#[interpreter_builtin(name = "Sys.timezone")]
1669fn builtin_sys_timezone(
1670 _args: &[RValue],
1671 _named: &[(String, RValue)],
1672 context: &BuiltinContext,
1673) -> Result<RValue, RError> {
1674 let tz = context
1675 .with_interpreter(|interp| interp.get_env_var("TZ"))
1676 .unwrap_or_else(|| "UTC".to_string());
1677 Ok(RValue::vec(Vector::Character(vec![Some(tz)].into())))
1678}
1679
1680#[builtin]
1684fn builtin_capabilities(_args: &[RValue], _named: &[(String, RValue)]) -> Result<RValue, RError> {
1685 let caps = vec![
1686 ("jpeg", false),
1687 ("png", false),
1688 ("tiff", false),
1689 ("tcltk", false),
1690 ("X11", false),
1691 ("aqua", false),
1692 ("http/ftp", false),
1693 ("sockets", false),
1694 ("libxml", false),
1695 ("fifo", cfg!(unix)),
1696 ("cledit", true),
1697 ("iconv", true),
1698 ("NLS", false),
1699 ("profmem", false),
1700 ("cairo", false),
1701 ("ICU", false),
1702 ("long.double", true),
1703 ("libcurl", false),
1704 ];
1705
1706 let names: Vec<Option<String>> = caps.iter().map(|(n, _)| Some(n.to_string())).collect();
1707 let values: Vec<Option<bool>> = caps.iter().map(|(_, v)| Some(*v)).collect();
1708
1709 let mut rv = RVector::from(Vector::Logical(values.into()));
1710 rv.set_attr(
1711 "names".to_string(),
1712 RValue::vec(Vector::Character(names.into())),
1713 );
1714 Ok(RValue::Vector(rv))
1715}
1716
1717#[builtin]
1721fn builtin_l10n_info(_args: &[RValue], _named: &[(String, RValue)]) -> Result<RValue, RError> {
1722 Ok(RValue::List(RList::new(vec![
1723 (
1724 Some("MBCS".to_string()),
1725 RValue::vec(Vector::Logical(vec![Some(true)].into())),
1726 ),
1727 (
1728 Some("UTF-8".to_string()),
1729 RValue::vec(Vector::Logical(vec![Some(true)].into())),
1730 ),
1731 (
1732 Some("Latin-1".to_string()),
1733 RValue::vec(Vector::Logical(vec![Some(false)].into())),
1734 ),
1735 ])))
1736}
1737
1738#[interpreter_builtin(name = "proc.time")]
1748fn interp_proc_time(
1749 _args: &[RValue],
1750 _named: &[(String, RValue)],
1751 context: &BuiltinContext,
1752) -> Result<RValue, RError> {
1753 let elapsed = context.with_interpreter(|interp| interp.start_instant.elapsed().as_secs_f64());
1754 Ok(make_proc_time(0.0, 0.0, elapsed))
1755}
1756
1757#[interpreter_builtin(name = "print.proc_time", min_args = 1)]
1768fn interp_print_proc_time(
1769 args: &[RValue],
1770 _named: &[(String, RValue)],
1771 context: &BuiltinContext,
1772) -> Result<RValue, RError> {
1773 let val = args
1774 .first()
1775 .ok_or_else(|| RError::new(RErrorKind::Argument, "argument is missing".to_string()))?;
1776 let (user, system, elapsed) = match val {
1777 RValue::Vector(rv) => match &rv.inner {
1778 Vector::Double(d) => {
1779 let u = d.first_opt().unwrap_or(0.0);
1780 let s = d.get_opt(1).unwrap_or(0.0);
1781 let e = d.get_opt(2).unwrap_or(0.0);
1782 (u, s, e)
1783 }
1784 _ => (0.0, 0.0, 0.0),
1785 },
1786 _ => (0.0, 0.0, 0.0),
1787 };
1788 context.write(&format!(
1789 " user system elapsed\n {:.3} {:.3} {:.3}\n",
1790 user, system, elapsed
1791 ));
1792 context.interpreter().set_invisible();
1793 Ok(val.clone())
1794}
1795
1796pub(super) fn make_proc_time(user: f64, system: f64, elapsed: f64) -> RValue {
1798 let mut rv = RVector::from(Vector::Double(
1799 vec![Some(user), Some(system), Some(elapsed)].into(),
1800 ));
1801 rv.set_attr(
1802 "names".to_string(),
1803 RValue::vec(Vector::Character(
1804 vec![
1805 Some("user.self".to_string()),
1806 Some("sys.self".to_string()),
1807 Some("elapsed".to_string()),
1808 ]
1809 .into(),
1810 )),
1811 );
1812 rv.set_attr(
1813 "class".to_string(),
1814 RValue::vec(Vector::Character(
1815 vec![Some("proc_time".to_string())].into(),
1816 )),
1817 );
1818 RValue::Vector(rv)
1819}
1820
1821#[interpreter_builtin(name = "sessionInfo")]
1827fn builtin_session_info(
1828 _args: &[RValue],
1829 _named: &[(String, RValue)],
1830 context: &BuiltinContext,
1831) -> Result<RValue, RError> {
1832 let locale = context
1833 .with_interpreter(|interp| interp.get_env_var("LANG"))
1834 .unwrap_or_else(|| "C".to_string());
1835 Ok(RValue::List(RList::new(vec![
1836 (
1837 Some("R.version".to_string()),
1838 RValue::List(RList::new(vec![
1839 (
1840 Some("major".to_string()),
1841 RValue::vec(Vector::Character(vec![Some("0".to_string())].into())),
1842 ),
1843 (
1844 Some("minor".to_string()),
1845 RValue::vec(Vector::Character(vec![Some("1.0".to_string())].into())),
1846 ),
1847 (
1848 Some("engine".to_string()),
1849 RValue::vec(Vector::Character(
1850 vec![Some("miniR (Rust)".to_string())].into(),
1851 )),
1852 ),
1853 ])),
1854 ),
1855 (
1856 Some("platform".to_string()),
1857 RValue::vec(Vector::Character(
1858 vec![Some(format!(
1859 "{}-{}",
1860 std::env::consts::ARCH,
1861 std::env::consts::OS
1862 ))]
1863 .into(),
1864 )),
1865 ),
1866 (
1867 Some("locale".to_string()),
1868 RValue::vec(Vector::Character(vec![Some(locale)].into())),
1869 ),
1870 ])))
1871}
1872
1873#[interpreter_builtin(name = "system.file")]
1883fn interp_system_file(
1884 args: &[RValue],
1885 named: &[(String, RValue)],
1886 context: &BuiltinContext,
1887) -> Result<RValue, RError> {
1888 let package = named
1890 .iter()
1891 .find(|(n, _)| n == "package")
1892 .and_then(|(_, v)| v.as_vector()?.as_character_scalar());
1893
1894 let lib_loc: Option<Vec<String>> =
1896 named
1897 .iter()
1898 .find(|(n, _)| n == "lib.loc")
1899 .and_then(|(_, v)| {
1900 let vec = v.as_vector()?;
1901 Some(
1902 vec.to_characters()
1903 .into_iter()
1904 .flatten()
1905 .collect::<Vec<String>>(),
1906 )
1907 });
1908
1909 let path_parts: Vec<String> = args
1911 .iter()
1912 .filter_map(|v| v.as_vector()?.as_character_scalar())
1913 .collect();
1914
1915 let package_name = match package {
1916 Some(p) if !p.is_empty() => p,
1917 _ => {
1918 return Ok(RValue::vec(Vector::Character(
1920 vec![Some(String::new())].into(),
1921 )));
1922 }
1923 };
1924
1925 let subpath = if path_parts.is_empty() {
1927 String::new()
1928 } else {
1929 path_parts.join("/")
1930 };
1931
1932 let result = context.with_interpreter(|interp| {
1934 let lib_paths = lib_loc.unwrap_or_else(|| interp.get_lib_paths());
1936
1937 for lib_path in &lib_paths {
1938 let pkg_dir = std::path::Path::new(lib_path).join(&package_name);
1939 if !pkg_dir.join("DESCRIPTION").is_file() {
1940 continue;
1941 }
1942 if subpath.is_empty() {
1943 return pkg_dir.to_string_lossy().to_string();
1945 }
1946 let target = pkg_dir.join(&subpath);
1947 if target.exists() {
1948 return target.to_string_lossy().to_string();
1949 }
1950 }
1951
1952 if let Some(ns) = interp.loaded_namespaces.borrow().get(&package_name) {
1954 let pkg_dir = &ns.lib_path;
1955 if subpath.is_empty() {
1956 return pkg_dir.to_string_lossy().to_string();
1957 }
1958 let target = pkg_dir.join(&subpath);
1959 if target.exists() {
1960 return target.to_string_lossy().to_string();
1961 }
1962 }
1963
1964 String::new()
1966 });
1967
1968 Ok(RValue::vec(Vector::Character(vec![Some(result)].into())))
1969}
1970
1971#[interpreter_builtin(name = "find.package", min_args = 1)]
1979fn interp_find_package(
1980 args: &[RValue],
1981 named: &[(String, RValue)],
1982 context: &BuiltinContext,
1983) -> Result<RValue, RError> {
1984 let packages = match &args[0] {
1985 RValue::Vector(rv) => rv.inner.to_characters(),
1986 _ => vec![],
1987 };
1988 let quiet = named
1989 .iter()
1990 .find(|(n, _)| n == "quiet")
1991 .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
1992 .unwrap_or(false);
1993
1994 let lib_loc: Option<Vec<String>> =
1995 named
1996 .iter()
1997 .find(|(n, _)| n == "lib.loc")
1998 .and_then(|(_, v)| {
1999 let vec = v.as_vector()?;
2000 Some(vec.to_characters().into_iter().flatten().collect())
2001 });
2002
2003 let results: Vec<Option<String>> = context.with_interpreter(|interp| {
2004 let lib_paths = lib_loc.unwrap_or_else(|| interp.get_lib_paths());
2005 packages
2006 .iter()
2007 .map(|pkg_opt| {
2008 let pkg = pkg_opt.as_deref().unwrap_or("");
2009 if crate::interpreter::Interpreter::is_base_package(pkg) {
2010 for lp in &lib_paths {
2015 let pkg_dir = std::path::Path::new(lp).join(pkg);
2016 if pkg_dir.is_dir() {
2017 return Some(pkg_dir.to_string_lossy().to_string());
2018 }
2019 }
2020 return Some(format!("lib/{pkg}"));
2022 }
2023 for lib_path in &lib_paths {
2024 let pkg_dir = std::path::Path::new(lib_path).join(pkg);
2025 if pkg_dir.join("DESCRIPTION").is_file() {
2026 return Some(pkg_dir.to_string_lossy().to_string());
2027 }
2028 }
2029 None
2030 })
2031 .collect()
2032 });
2033
2034 if results.iter().any(|r| r.is_none()) && !quiet {
2035 let missing: Vec<&str> = packages
2036 .iter()
2037 .zip(&results)
2038 .filter(|(_, r)| r.is_none())
2039 .map(|(p, _)| p.as_deref().unwrap_or(""))
2040 .collect();
2041 return Err(RError::new(
2042 RErrorKind::Other,
2043 format!("there is no package called '{}'", missing.join("', '")),
2044 ));
2045 }
2046
2047 Ok(RValue::vec(Vector::Character(results.into())))
2048}
2049
2050#[builtin(name = "Sys.getpid")]
2054fn builtin_sys_getpid(_args: &[RValue], _named: &[(String, RValue)]) -> Result<RValue, RError> {
2055 let pid = i64::from(std::process::id());
2056 Ok(RValue::vec(Vector::Integer(vec![Some(pid)].into())))
2057}
2058
2059#[builtin(name = "shell.exec", min_args = 1)]
2068fn builtin_shell_exec(args: &[RValue], _named: &[(String, RValue)]) -> Result<RValue, RError> {
2069 let file = args
2070 .first()
2071 .and_then(|v| v.as_vector()?.as_character_scalar())
2072 .ok_or_else(|| {
2073 RError::new(
2074 RErrorKind::Argument,
2075 "'file' must be a character string".to_string(),
2076 )
2077 })?;
2078
2079 let result = if cfg!(target_os = "macos") {
2080 std::process::Command::new("open").arg(&file).spawn()
2081 } else if cfg!(target_os = "linux") {
2082 std::process::Command::new("xdg-open").arg(&file).spawn()
2083 } else if cfg!(target_os = "windows") {
2084 std::process::Command::new("cmd")
2085 .args(["/c", "start", "", &file])
2086 .spawn()
2087 } else {
2088 return Err(RError::other(
2089 "shell.exec is not supported on this platform".to_string(),
2090 ));
2091 };
2092
2093 match result {
2094 Ok(_) => Ok(RValue::Null),
2095 Err(e) => Err(RError::other(format!("cannot open '{}': {}", file, e))),
2096 }
2097}