1use crate::{
3 core::{
4 cons::Cons,
5 env::{Env, sym},
6 error::{Type, TypeError},
7 gc::{Context, Rt},
8 object::{Number, Object, ObjectType, OptionalFlag, Symbol},
9 },
10 data::symbol_value,
11 lisp::maybe_quit,
12 search::lisp_regex_to_rust,
13 sym::{FILE_NAME_HANDLER_ALIST, INHIBIT_FILE_NAME_HANDLERS, NIL},
14};
15use anyhow::{Result, bail, ensure};
16use fancy_regex::Regex;
17use rune_macros::defun;
18use std::path::{Component, MAIN_SEPARATOR, Path};
19
20#[defun]
21pub(crate) fn expand_file_name(
22 name: &str,
23 default_directory: Option<&str>,
24 env: &Rt<Env>,
25 cx: &Context,
26) -> Result<String> {
27 if Path::new(name).is_absolute() {
30 Ok(name.to_owned())
31 } else if let Some(dir) = default_directory {
32 let path = Path::new(dir);
33 Ok(path.join(name).to_string_lossy().to_string())
34 } else {
35 let dir = env.vars.get(sym::DEFAULT_DIRECTORY).unwrap();
36 match dir.untag(cx) {
37 ObjectType::String(dir) => {
38 let path = Path::new(dir.as_ref());
39 Ok(path.join(name).to_string_lossy().to_string())
40 }
41 _ => unreachable!("`default-directory' should be a string"),
42 }
43 }
44}
45
46#[defun]
47fn car_less_than_car(a: &Cons, b: &Cons) -> Result<bool> {
48 let a: Number = a.car().try_into()?;
49 let b: Number = b.car().try_into()?;
50 Ok(a.val() < b.val())
51}
52
53#[defun]
54fn file_name_as_directory(filename: &str) -> String {
55 if filename.ends_with(MAIN_SEPARATOR) {
56 filename.to_owned()
57 } else {
58 format!("{filename}{MAIN_SEPARATOR}")
59 }
60}
61
62#[defun]
63fn file_directory_p(filename: &str) -> bool {
64 if filename.is_empty() { true } else { Path::new(filename).is_dir() }
65}
66
67#[defun]
69fn directory_file_name(dirname: &str) -> &str {
70 let path = Path::new(dirname);
71 let mut path_components = path.components();
72 if path_components.clone().next().is_none() {
73 return "";
74 }
75
76 if path_components.all(|c| c == Component::RootDir || c == Component::Normal("".as_ref())) {
77 return "/";
78 }
79
80 dirname.strip_suffix(MAIN_SEPARATOR).unwrap_or(dirname)
81}
82
83#[defun]
85fn file_name_absolute_p(filename: &str) -> bool {
86 let path = Path::new(filename);
87 path.is_absolute()
92}
93
94#[defun]
96fn file_name_directory(filename: &str) -> Option<String> {
97 if !filename.contains(MAIN_SEPARATOR) {
100 return None;
101 }
102
103 if filename.ends_with(MAIN_SEPARATOR) {
104 return Some(filename.into());
105 }
106
107 let path = Path::new(filename);
108 let parent = path.parent()?;
109
110 if parent.parent().is_none() {
112 return Some(format!("{MAIN_SEPARATOR}"));
113 }
114 let parent_path = parent.to_str()?;
115 Some(format!("{parent_path}{MAIN_SEPARATOR}"))
116}
117
118#[defun]
120fn file_name_nondirectory(filename: &str) -> &str {
121 if filename.ends_with(MAIN_SEPARATOR) {
122 return "";
123 }
124
125 let path = Path::new(filename);
126 let Some(file_name) = path.file_name() else {
127 return "";
128 };
129
130 file_name.to_str().unwrap()
131}
132
133#[defun]
135fn directory_name_p(name: &str) -> bool {
136 name.ends_with(MAIN_SEPARATOR)
137}
138
139defvar!(FILE_NAME_HANDLER_ALIST);
140defvar!(INHIBIT_FILE_NAME_HANDLERS);
141defsym!(INHIBIT_FILE_NAME_OPERATION);
142
143#[defun]
153fn find_file_name_handler<'ob>(
154 filename: &str,
155 operation: Symbol,
156 env: &mut Rt<Env>,
157 cx: &'ob Context,
158) -> Symbol<'ob> {
159 find_file_name_handler_internal(filename, operation, env, cx).unwrap_or(NIL)
160}
161
162fn find_file_name_handler_internal<'ob>(
163 filename: &str,
164 operation: Symbol,
165 env: &mut Rt<Env>,
166 cx: &'ob Context,
167) -> Option<Symbol<'ob>> {
168 let file_name_handler_alist = symbol_value(FILE_NAME_HANDLER_ALIST, env, cx)?.as_list().ok()?;
169 let inhibit_file_name_handlers =
170 symbol_value(INHIBIT_FILE_NAME_HANDLERS, env, cx).and_then(|sym| sym.as_list().ok());
171
172 file_name_handler_alist
173 .filter_map(|elem| {
174 let cons = elem.ok()?.cons()?;
175 let regexp = cons.car().string()?;
176 let re = Regex::new(&lisp_regex_to_rust(regexp))
177 .unwrap_or_else(|err| panic!("Invalid regexp '{}': {}", regexp, err));
178 if re.is_match(filename).ok()? {
179 let handler = cons.cdr().symbol()?;
180 if operation != sym::INHIBIT_FILE_NAME_OPERATION
181 || !inhibit_file_name_handlers
182 .clone()
183 .is_some_and(|mut ifnh| ifnh.any(|h| h.is_ok_and(|h| h == handler)))
184 {
185 return Some(handler);
186 }
187 }
188 maybe_quit();
189 None
190 })
191 .next()
192}
193
194#[defun]
195fn file_symlink_p(filename: &str) -> bool {
196 Path::new(filename).is_symlink()
197}
198
199#[defun]
200fn file_name_case_insensitive_p(filename: &str) -> bool {
201 if !Path::new(filename).exists() {
202 return false;
203 }
204 case_insensitive(filename)
205}
206
207#[cfg(target_os = "macos")]
208fn case_insensitive(filename: &str) -> bool {
209 const _PC_CASE_SENSITIVE: libc::c_int = 11;
211 let result = unsafe {
212 let filename = std::ffi::CString::new(filename).unwrap();
213 libc::pathconf(filename.as_ptr(), _PC_CASE_SENSITIVE)
214 };
215 if result == -1 {
216 panic!(
217 "file-name-case-insensitive-p pathconf failed: {}",
218 std::io::Error::last_os_error()
219 )
220 }
221 result == 0
222}
223
224#[cfg(windows)]
225fn case_insensitive(filename: &str) -> bool {
226 let output = std::process::Command::new("fsutil.exe")
228 .arg("file")
229 .arg("queryCaseSensitiveInfo")
230 .arg(filename)
231 .output()
232 .unwrap()
233 .stdout;
234 std::str::from_utf8(&output).unwrap().contains("disabled")
235}
236
237#[cfg(target_os = "linux")]
238fn case_insensitive(_filename: &str) -> bool {
239 false
240}
241
242#[test]
243#[cfg(not(miri))]
244fn test_case_sensative_call() {
245 let _ = file_name_case_insensitive_p("/");
246}
247
248#[defun]
249#[expect(clippy::too_many_arguments)]
250fn write_region(
251 start: i64,
252 end: i64,
253 filename: &str,
254 append: OptionalFlag,
255 visit: OptionalFlag,
256 lockname: OptionalFlag,
257 mustbenew: OptionalFlag,
258 env: &Rt<Env>,
259) -> Result<()> {
260 use std::io::Write;
261 ensure!(append.is_none(), "append not implemented");
262 ensure!(visit.is_none(), "visit not implemented");
263 ensure!(lockname.is_none(), "lockname not implemented");
264 ensure!(mustbenew.is_none(), "mustbenew not implemented");
265 let mut file = std::fs::OpenOptions::new()
267 .write(true)
268 .create(true)
269 .truncate(true)
270 .open(filename)
271 .unwrap();
272 let b = env.current_buffer.get();
273 let (s1, s2) = b.slice_with_gap(start as usize, end as usize)?;
274 write!(file, "{s1}")?;
275 write!(file, "{s2}")?;
276 Ok(())
277}
278
279#[defun]
281fn file_name_concat(directory: &str, rest_components: &[Object]) -> Result<String> {
282 let mut path = String::from(directory);
283
284 for r_c in rest_components {
286 let ObjectType::String(s) = r_c.untag() else {
287 bail!(TypeError::new(Type::String, r_c));
288 };
289
290 if !path.ends_with(MAIN_SEPARATOR) {
293 path.push(MAIN_SEPARATOR)
294 }
295
296 path.push_str(s.as_ref());
297 }
298
299 Ok(path)
300}
301
302#[cfg(test)]
307mod tests {
308 use super::*;
309 use crate::{core::gc::RootSet, data::set, sym::INHIBIT_FILE_NAME_HANDLERS};
310 use rune_core::macros::{list, root};
311
312 #[test]
313 fn test_find_file_name_handler_matching() {
314 let roots = &RootSet::default();
315 let cx = &mut Context::new(roots);
316 root!(env, new(Env), cx);
317
318 let handler = Symbol::new_uninterned("matching-handler", cx);
319 let not_matching_handler = Symbol::new_uninterned("not-matching-handler", cx);
320 let file_name_handler_alist = list![
321 Cons::new(r"test\.txt", handler, cx),
322 Cons::new("NEVER-MATCH", not_matching_handler, cx);
323 cx
324 ];
325 set(FILE_NAME_HANDLER_ALIST, file_name_handler_alist, env).unwrap();
326
327 let test_txt_handler = find_file_name_handler("test.txt", NIL, env, cx);
328 assert_eq!(test_txt_handler, handler);
329 let not_matching_handler = find_file_name_handler("not-matching.el", NIL, env, cx);
330 assert_eq!(not_matching_handler, NIL);
331 }
332
333 #[test]
334 fn test_find_file_name_handler_inhibit_handlers() {
335 let roots = &RootSet::default();
336 let cx = &mut Context::new(roots);
337 root!(env, new(Env), cx);
338
339 let handler = Symbol::new_uninterned("matching-handler", cx);
340 let inhibit_handler = Symbol::new_uninterned("inhibit-handler", cx);
341 let file_name_handler_alist = list![
342 Cons::new(r"test\.txt", inhibit_handler, cx),
343 Cons::new(r"test\.txt", handler, cx);
344 cx
345 ];
346 set(FILE_NAME_HANDLER_ALIST, file_name_handler_alist, env).unwrap();
347 set(INHIBIT_FILE_NAME_HANDLERS, list![inhibit_handler; cx], env).unwrap();
348
349 let result = find_file_name_handler("test.txt", NIL, env, cx);
350 assert_eq!(result, inhibit_handler);
351
352 let result = find_file_name_handler("test.txt", sym::INHIBIT_FILE_NAME_OPERATION, env, cx);
353 assert_eq!(result, handler);
354 }
355
356 #[test]
357 fn test_find_file_name_handler_no_panic() {
358 let roots = &RootSet::default();
359 let cx = &mut Context::new(roots);
360 let handler = Symbol::new_uninterned("handler", cx);
361 root!(env, new(Env), cx);
362
363 set(FILE_NAME_HANDLER_ALIST, list![Cons::new(NIL, handler, cx); cx], env).unwrap();
364 find_file_name_handler("example", NIL, env, cx);
365
366 set(FILE_NAME_HANDLER_ALIST, list![Cons::new(".*", NIL , cx); cx], env).unwrap();
367 find_file_name_handler("example", NIL, env, cx);
368 }
369
370 #[test]
371 #[should_panic(
372 expected = "Invalid regexp '\\\\(incorrect-regexp': Parsing error at position 17: Opening parenthesis without closing parenthesis"
373 )]
374 fn test_find_file_name_handler_panic_on_invalid_regex() {
375 let roots = &RootSet::default();
376 let cx = &mut Context::new(roots);
377 let handler = Symbol::new_uninterned("handler", cx);
378 root!(env, new(Env), cx);
379
380 set(
381 FILE_NAME_HANDLER_ALIST,
382 list![Cons::new(r#"\(incorrect-regexp"#, handler, cx); cx],
383 env,
384 )
385 .unwrap();
386 find_file_name_handler("example", NIL, env, cx);
387 }
388}