1use crate::core::{
3 cons::Cons,
4 env::{Env, sym},
5 error::{Type, TypeError},
6 gc::{Context, Rt},
7 object::{Number, Object, ObjectType, OptionalFlag},
8};
9use anyhow::{Result, bail, ensure};
10use rune_macros::defun;
11use std::path::{Component, MAIN_SEPARATOR, Path};
12
13defvar!(FILE_NAME_HANDLER_ALIST);
14
15#[defun]
16pub(crate) fn expand_file_name(
17 name: &str,
18 default_directory: Option<&str>,
19 env: &Rt<Env>,
20 cx: &Context,
21) -> Result<String> {
22 if Path::new(name).is_absolute() {
25 Ok(name.to_owned())
26 } else if let Some(dir) = default_directory {
27 let path = Path::new(dir);
28 Ok(path.join(name).to_string_lossy().to_string())
29 } else {
30 let dir = env.vars.get(sym::DEFAULT_DIRECTORY).unwrap();
31 match dir.untag(cx) {
32 ObjectType::String(dir) => {
33 let path = Path::new(dir.as_ref());
34 Ok(path.join(name).to_string_lossy().to_string())
35 }
36 _ => unreachable!("`default-directory' should be a string"),
37 }
38 }
39}
40
41#[defun]
42fn car_less_than_car(a: &Cons, b: &Cons) -> Result<bool> {
43 let a: Number = a.car().try_into()?;
44 let b: Number = b.car().try_into()?;
45 Ok(a.val() < b.val())
46}
47
48#[defun]
49fn file_name_as_directory(filename: &str) -> String {
50 if filename.ends_with(MAIN_SEPARATOR) {
51 filename.to_owned()
52 } else {
53 format!("{filename}{MAIN_SEPARATOR}")
54 }
55}
56
57#[defun]
58fn file_directory_p(filename: &str) -> bool {
59 if filename.is_empty() { true } else { Path::new(filename).is_dir() }
60}
61
62#[defun]
64fn directory_file_name(dirname: &str) -> &str {
65 let path = Path::new(dirname);
66 let mut path_components = path.components();
67 if path_components.clone().next().is_none() {
68 return "";
69 }
70
71 if path_components.all(|c| c == Component::RootDir || c == Component::Normal("".as_ref())) {
72 return "/";
73 }
74
75 dirname.strip_suffix(MAIN_SEPARATOR).unwrap_or(dirname)
76}
77
78#[defun]
80fn file_name_absolute_p(filename: &str) -> bool {
81 let path = Path::new(filename);
82 path.is_absolute()
87}
88
89#[defun]
91fn file_name_directory(filename: &str) -> Option<String> {
92 if !filename.contains(MAIN_SEPARATOR) {
95 return None;
96 }
97
98 if filename.ends_with(MAIN_SEPARATOR) {
99 return Some(filename.into());
100 }
101
102 let path = Path::new(filename);
103 let parent = path.parent()?;
104
105 if parent.parent().is_none() {
107 return Some(format!("{MAIN_SEPARATOR}"));
108 }
109 let parent_path = parent.to_str()?;
110 Some(format!("{parent_path}{MAIN_SEPARATOR}"))
111}
112
113#[defun]
115fn file_name_nondirectory(filename: &str) -> &str {
116 if filename.ends_with(MAIN_SEPARATOR) {
117 return "";
118 }
119
120 let path = Path::new(filename);
121 let Some(file_name) = path.file_name() else {
122 return "";
123 };
124
125 file_name.to_str().unwrap()
126}
127
128#[defun]
130fn directory_name_p(name: &str) -> bool {
131 name.ends_with(MAIN_SEPARATOR)
132}
133
134#[defun]
135fn find_file_name_handler(_filename: &str, _operation: Object) {
136 }
138
139#[defun]
140fn file_symlink_p(filename: &str) -> bool {
141 Path::new(filename).is_symlink()
142}
143
144#[defun]
145fn file_name_case_insensitive_p(filename: &str) -> bool {
146 if !Path::new(filename).exists() {
147 return false;
148 }
149 case_insensitive(filename)
150}
151
152#[cfg(target_os = "macos")]
153fn case_insensitive(filename: &str) -> bool {
154 const _PC_CASE_SENSITIVE: libc::c_int = 11;
156 let result = unsafe {
157 let filename = std::ffi::CString::new(filename).unwrap();
158 libc::pathconf(filename.as_ptr(), _PC_CASE_SENSITIVE)
159 };
160 if result == -1 {
161 panic!(
162 "file-name-case-insensitive-p pathconf failed: {}",
163 std::io::Error::last_os_error()
164 )
165 }
166 result == 0
167}
168
169#[cfg(windows)]
170fn case_insensitive(filename: &str) -> bool {
171 let output = std::process::Command::new("fsutil.exe")
173 .arg("file")
174 .arg("queryCaseSensitiveInfo")
175 .arg(filename)
176 .output()
177 .unwrap()
178 .stdout;
179 std::str::from_utf8(&output).unwrap().contains("disabled")
180}
181
182#[cfg(target_os = "linux")]
183fn case_insensitive(_filename: &str) -> bool {
184 false
185}
186
187#[test]
188#[cfg(not(miri))]
189fn test_case_sensative_call() {
190 let _ = file_name_case_insensitive_p("/");
191}
192
193#[defun]
194#[expect(clippy::too_many_arguments)]
195fn write_region(
196 start: i64,
197 end: i64,
198 filename: &str,
199 append: OptionalFlag,
200 visit: OptionalFlag,
201 lockname: OptionalFlag,
202 mustbenew: OptionalFlag,
203 env: &Rt<Env>,
204) -> Result<()> {
205 use std::io::Write;
206 ensure!(append.is_none(), "append not implemented");
207 ensure!(visit.is_none(), "visit not implemented");
208 ensure!(lockname.is_none(), "lockname not implemented");
209 ensure!(mustbenew.is_none(), "mustbenew not implemented");
210 let mut file = std::fs::OpenOptions::new()
212 .write(true)
213 .create(true)
214 .truncate(true)
215 .open(filename)
216 .unwrap();
217 let b = env.current_buffer.get();
218 let (s1, s2) = b.slice_with_gap(start as usize, end as usize)?;
219 write!(file, "{s1}")?;
220 write!(file, "{s2}")?;
221 Ok(())
222}
223
224#[defun]
226fn file_name_concat(directory: &str, rest_components: &[Object]) -> Result<String> {
227 let mut path = String::from(directory);
228
229 for r_c in rest_components {
231 let ObjectType::String(s) = r_c.untag() else {
232 bail!(TypeError::new(Type::String, r_c));
233 };
234
235 if !path.ends_with(MAIN_SEPARATOR) {
238 path.push(MAIN_SEPARATOR)
239 }
240
241 path.push_str(s.as_ref());
242 }
243
244 Ok(path)
245}
246
247