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_accessible_directory_p(filename: &str) -> bool {
55 let path = Path::new(filename);
56 path.exists() && path.is_dir()
57}
58
59#[defun]
60fn file_name_as_directory(filename: &str) -> String {
61 if filename.ends_with(MAIN_SEPARATOR) {
62 filename.to_owned()
63 } else {
64 format!("{filename}{MAIN_SEPARATOR}")
65 }
66}
67
68#[defun]
69fn file_directory_p(filename: &str) -> bool {
70 if filename.is_empty() { true } else { Path::new(filename).is_dir() }
71}
72
73#[defun]
75fn directory_file_name(dirname: &str) -> &str {
76 let path = Path::new(dirname);
77 let mut path_components = path.components();
78 if path_components.clone().next().is_none() {
79 return "";
80 }
81
82 if path_components.all(|c| c == Component::RootDir || c == Component::Normal("".as_ref())) {
83 return "/";
84 }
85
86 dirname.strip_suffix(MAIN_SEPARATOR).unwrap_or(dirname)
87}
88
89#[defun]
91fn file_name_absolute_p(filename: &str) -> bool {
92 let path = Path::new(filename);
93 path.is_absolute()
98}
99
100#[defun]
102fn file_name_directory(filename: &str) -> Option<String> {
103 if !filename.contains(MAIN_SEPARATOR) {
106 return None;
107 }
108
109 if filename.ends_with(MAIN_SEPARATOR) {
110 return Some(filename.into());
111 }
112
113 let path = Path::new(filename);
114 let parent = path.parent()?;
115
116 if parent.parent().is_none() {
118 return Some(format!("{MAIN_SEPARATOR}"));
119 }
120 let parent_path = parent.to_str()?;
121 Some(format!("{parent_path}{MAIN_SEPARATOR}"))
122}
123
124#[defun]
126fn file_name_nondirectory(filename: &str) -> &str {
127 if filename.ends_with(MAIN_SEPARATOR) {
128 return "";
129 }
130
131 let path = Path::new(filename);
132 let Some(file_name) = path.file_name() else {
133 return "";
134 };
135
136 file_name.to_str().unwrap()
137}
138
139#[defun]
141fn directory_name_p(name: &str) -> bool {
142 name.ends_with(MAIN_SEPARATOR)
143}
144
145defvar!(FILE_NAME_HANDLER_ALIST);
146defvar!(INHIBIT_FILE_NAME_HANDLERS);
147defsym!(INHIBIT_FILE_NAME_OPERATION);
148
149#[defun]
159fn find_file_name_handler<'ob>(
160 filename: &str,
161 operation: Symbol,
162 env: &mut Rt<Env>,
163 cx: &'ob Context,
164) -> Symbol<'ob> {
165 find_file_name_handler_internal(filename, operation, env, cx).unwrap_or(NIL)
166}
167
168fn find_file_name_handler_internal<'ob>(
169 filename: &str,
170 operation: Symbol,
171 env: &mut Rt<Env>,
172 cx: &'ob Context,
173) -> Option<Symbol<'ob>> {
174 let file_name_handler_alist = symbol_value(FILE_NAME_HANDLER_ALIST, env, cx)?.as_list().ok()?;
175 let inhibit_file_name_handlers =
176 symbol_value(INHIBIT_FILE_NAME_HANDLERS, env, cx).and_then(|sym| sym.as_list().ok());
177
178 file_name_handler_alist
179 .filter_map(|elem| {
180 let cons = elem.ok()?.cons()?;
181 let regexp = cons.car().string()?;
182 let re = Regex::new(&lisp_regex_to_rust(regexp))
183 .unwrap_or_else(|err| panic!("Invalid regexp '{regexp}': {err}"));
184 if re.is_match(filename).ok()? {
185 let handler = cons.cdr().symbol()?;
186 if operation != sym::INHIBIT_FILE_NAME_OPERATION
187 || !inhibit_file_name_handlers
188 .clone()
189 .is_some_and(|mut ifnh| ifnh.any(|h| h.is_ok_and(|h| h == handler)))
190 {
191 return Some(handler);
192 }
193 }
194 maybe_quit();
195 None
196 })
197 .next()
198}
199
200#[defun]
201fn file_symlink_p(filename: &str) -> bool {
202 Path::new(filename).is_symlink()
203}
204
205#[defun]
206fn file_name_case_insensitive_p(filename: &str) -> bool {
207 if !Path::new(filename).exists() {
208 return false;
209 }
210 case_insensitive(filename)
211}
212
213#[cfg(target_os = "macos")]
214fn case_insensitive(filename: &str) -> bool {
215 const _PC_CASE_SENSITIVE: libc::c_int = 11;
217 let result = unsafe {
218 let filename = std::ffi::CString::new(filename).unwrap();
219 libc::pathconf(filename.as_ptr(), _PC_CASE_SENSITIVE)
220 };
221 if result == -1 {
222 panic!(
223 "file-name-case-insensitive-p pathconf failed: {}",
224 std::io::Error::last_os_error()
225 )
226 }
227 result == 0
228}
229
230#[cfg(windows)]
231fn case_insensitive(filename: &str) -> bool {
232 let output = std::process::Command::new("fsutil.exe")
234 .arg("file")
235 .arg("queryCaseSensitiveInfo")
236 .arg(filename)
237 .output()
238 .unwrap()
239 .stdout;
240 std::str::from_utf8(&output).unwrap().contains("disabled")
241}
242
243#[cfg(target_os = "linux")]
244fn case_insensitive(_filename: &str) -> bool {
245 false
246}
247
248#[test]
249#[cfg(not(miri))]
250fn test_case_sensative_call() {
251 let _ = file_name_case_insensitive_p("/");
252}
253
254#[defun]
255#[expect(clippy::too_many_arguments)]
256fn write_region(
257 start: i64,
258 end: i64,
259 filename: &str,
260 append: OptionalFlag,
261 visit: OptionalFlag,
262 lockname: OptionalFlag,
263 mustbenew: OptionalFlag,
264 env: &Rt<Env>,
265) -> Result<()> {
266 use std::io::Write;
267 ensure!(append.is_none(), "append not implemented");
268 ensure!(visit.is_none(), "visit not implemented");
269 ensure!(lockname.is_none(), "lockname not implemented");
270 ensure!(mustbenew.is_none(), "mustbenew not implemented");
271 let mut file = std::fs::OpenOptions::new()
273 .write(true)
274 .create(true)
275 .truncate(true)
276 .open(filename)
277 .unwrap();
278 let b = env.current_buffer.get();
279 let (s1, s2) = b.slice_with_gap(start as usize, end as usize)?;
280 write!(file, "{s1}")?;
281 write!(file, "{s2}")?;
282 Ok(())
283}
284
285#[defun]
287fn file_name_concat(directory: &str, rest_components: &[Object]) -> Result<String> {
288 let mut path = String::from(directory);
289
290 for r_c in rest_components {
292 let ObjectType::String(s) = r_c.untag() else {
293 bail!(TypeError::new(Type::String, r_c));
294 };
295
296 if !path.ends_with(MAIN_SEPARATOR) {
299 path.push(MAIN_SEPARATOR)
300 }
301
302 path.push_str(s.as_ref());
303 }
304
305 Ok(path)
306}
307
308#[cfg(test)]
313mod tests {
314 use super::*;
315 use crate::{
316 assert_elprop,
317 core::{env::sym, gc::RootSet},
318 data::set,
319 sym::INHIBIT_FILE_NAME_HANDLERS,
320 };
321 use rune_core::macros::{list, root};
322
323 #[test]
324 fn test_find_file_name_handler_matching() {
325 let roots = &RootSet::default();
326 let cx = &mut Context::new(roots);
327 root!(env, new(Env), cx);
328
329 let handler = Symbol::new_uninterned("matching-handler", cx);
330 let not_matching_handler = Symbol::new_uninterned("not-matching-handler", cx);
331 let file_name_handler_alist = list![
332 Cons::new(r"test\.txt", handler, cx),
333 Cons::new("NEVER-MATCH", not_matching_handler, cx);
334 cx
335 ];
336 set(FILE_NAME_HANDLER_ALIST, file_name_handler_alist, env).unwrap();
337
338 let test_txt_handler = find_file_name_handler("test.txt", NIL, env, cx);
339 assert_eq!(test_txt_handler, handler);
340 let not_matching_handler = find_file_name_handler("not-matching.el", NIL, env, cx);
341 assert_eq!(not_matching_handler, NIL);
342 }
343
344 #[test]
345 fn test_find_file_name_handler_inhibit_handlers() {
346 let roots = &RootSet::default();
347 let cx = &mut Context::new(roots);
348 root!(env, new(Env), cx);
349
350 let handler = Symbol::new_uninterned("matching-handler", cx);
351 let inhibit_handler = Symbol::new_uninterned("inhibit-handler", cx);
352 let file_name_handler_alist = list![
353 Cons::new(r"test\.txt", inhibit_handler, cx),
354 Cons::new(r"test\.txt", handler, cx);
355 cx
356 ];
357 set(FILE_NAME_HANDLER_ALIST, file_name_handler_alist, env).unwrap();
358 set(INHIBIT_FILE_NAME_HANDLERS, list![inhibit_handler; cx], env).unwrap();
359
360 let result = find_file_name_handler("test.txt", NIL, env, cx);
361 assert_eq!(result, inhibit_handler);
362
363 let result = find_file_name_handler("test.txt", sym::INHIBIT_FILE_NAME_OPERATION, env, cx);
364 assert_eq!(result, handler);
365 }
366
367 #[test]
368 fn test_find_file_name_handler_no_panic() {
369 let roots = &RootSet::default();
370 let cx = &mut Context::new(roots);
371 let handler = Symbol::new_uninterned("handler", cx);
372 root!(env, new(Env), cx);
373
374 set(FILE_NAME_HANDLER_ALIST, list![Cons::new(NIL, handler, cx); cx], env).unwrap();
375 find_file_name_handler("example", NIL, env, cx);
376
377 set(FILE_NAME_HANDLER_ALIST, list![Cons::new(".*", NIL , cx); cx], env).unwrap();
378 find_file_name_handler("example", NIL, env, cx);
379 }
380
381 #[test]
382 #[should_panic(
383 expected = "Invalid regexp '\\\\(incorrect-regexp': Parsing error at position 17: Opening parenthesis without closing parenthesis"
384 )]
385 fn test_find_file_name_handler_panic_on_invalid_regex() {
386 let roots = &RootSet::default();
387 let cx = &mut Context::new(roots);
388 let handler = Symbol::new_uninterned("handler", cx);
389 root!(env, new(Env), cx);
390
391 set(
392 FILE_NAME_HANDLER_ALIST,
393 list![Cons::new(r#"\(incorrect-regexp"#, handler, cx); cx],
394 env,
395 )
396 .unwrap();
397 find_file_name_handler("example", NIL, env, cx);
398 }
399
400 #[test]
401 #[ignore = "TODO: Handle ~ in expand-file-name"]
402 fn test_expand_file_name() {
403 assert_elprop![r#"(expand-file-name "~/test.txt" "/")"#];
404 }
405}