use std::collections::{HashMap, VecDeque};
#[cfg(unix)]
use std::ffi::{CStr, CString};
use std::fs;
use std::io::{ErrorKind, Seek, SeekFrom, Write};
#[cfg(unix)]
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};
use std::sync::Once;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use super::error::{EvalResult, Flow, signal};
use super::eval::Context;
use super::intern::{intern, resolve_sym};
use super::symbol::Obarray;
use super::value::{OrderedRuntimeBindingMap, Value, ValueKind, VecLikeType, list_to_vec};
pub fn expand_file_name(name: &str, default_dir: Option<&str>) -> String {
let expanded = if name.starts_with("~/") {
if let Some(home) = std::env::var_os("HOME") {
let home_str = home.to_string_lossy();
format!("{}{}", home_str, &name[1..])
} else {
name.to_string()
}
} else if name == "~" {
if let Some(home) = std::env::var_os("HOME") {
home.to_string_lossy().into_owned()
} else {
name.to_string()
}
} else {
name.to_string()
};
let path = Path::new(&expanded);
let preserve_trailing_slash = expanded.ends_with('/');
if path.is_absolute() {
let mut cleaned = clean_path(&PathBuf::from(&expanded));
if preserve_trailing_slash && !cleaned.ends_with('/') {
cleaned.push('/');
}
return cleaned;
}
let base = if let Some(dir) = default_dir {
let expanded_dir = expand_file_name(dir, None);
PathBuf::from(expanded_dir)
} else {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))
};
let joined = base.join(&expanded);
let mut cleaned = clean_path(&joined);
if preserve_trailing_slash && !cleaned.ends_with('/') {
cleaned.push('/');
}
cleaned
}
pub(crate) fn lisp_file_name_to_path_buf(filename: &crate::heap_types::LispString) -> PathBuf {
#[cfg(unix)]
{
PathBuf::from(std::ffi::OsString::from_vec(filename.as_bytes().to_vec()))
}
#[cfg(not(unix))]
{
PathBuf::from(crate::emacs_core::builtins::runtime_string_from_lisp_string(filename))
}
}
pub(crate) fn path_to_lisp_file_name(path: &Path) -> crate::heap_types::LispString {
#[cfg(unix)]
{
crate::heap_types::LispString::from_unibyte(path.as_os_str().as_bytes().to_vec())
}
#[cfg(not(unix))]
{
crate::emacs_core::builtins::runtime_string_to_lisp_string(
path.to_string_lossy().as_ref(),
true,
)
}
}
fn canonicalize_with_missing_suffix(path: &Path) -> PathBuf {
if let Ok(canon) = fs::canonicalize(path) {
return canon;
}
let mut prefix = path.to_path_buf();
let mut suffix = VecDeque::new();
loop {
if let Ok(canon_prefix) = fs::canonicalize(&prefix) {
let mut resolved = canon_prefix;
for part in suffix {
resolved.push(part);
}
return resolved;
}
let Some(name) = prefix.file_name().map(|s| s.to_os_string()) else {
break;
};
suffix.push_front(name);
if !prefix.pop() {
break;
}
}
path.to_path_buf()
}
pub fn file_truename(filename: &str, default_dir: Option<&str>) -> String {
let expanded = expand_file_name(filename, default_dir);
let preserve_trailing_slash = expanded.ends_with('/');
let mut resolved = canonicalize_with_missing_suffix(Path::new(&expanded))
.to_string_lossy()
.into_owned();
if preserve_trailing_slash && resolved != "/" && !resolved.ends_with('/') {
resolved.push('/');
}
resolved
}
fn file_truename_lisp_inner(
filename: &crate::heap_types::LispString,
default_dir: &crate::heap_types::LispString,
remaining_links: &mut i64,
prev_dirs: &mut HashMap<Vec<u8>, crate::heap_types::LispString>,
) -> Result<crate::heap_types::LispString, Flow> {
let mut filename = if lisp_file_name_absolute_system_p(filename) {
filename.clone()
} else {
expand_file_name_lisp(filename, Some(default_dir))
};
loop {
*remaining_links -= 1;
if *remaining_links < 0 {
return Err(signal(
"error",
vec![Value::string(format!(
"Apparent cycle of symbolic links for {}",
crate::emacs_core::builtins::runtime_string_from_lisp_string(&filename)
))],
));
}
let mut dir = lisp_file_name_directory(&filename).unwrap_or_else(|| default_dir.clone());
let dirfile = lisp_directory_file_name(&dir);
if !lisp_string_runtime_eq(&dir, &dirfile) {
let dir_key = dir.as_bytes().to_vec();
if let Some(cached) = prev_dirs.get(&dir_key).cloned() {
dir = cached;
} else {
let new = lisp_file_name_as_directory(&file_truename_lisp_inner(
&dirfile,
default_dir,
remaining_links,
prev_dirs,
)?);
prev_dirs.insert(dir_key, new.clone());
dir = new;
}
}
let filename_no_dir = lisp_file_name_nondirectory(&filename);
if lisp_file_name_is_ascii_text(&filename_no_dir, b"..") {
let parent = lisp_directory_file_name(&dir);
return Ok(match lisp_file_name_directory(&parent) {
Some(parent_dir) => lisp_directory_file_name(&parent_dir),
None => parent,
});
}
if lisp_file_name_is_ascii_text(&filename_no_dir, b".") {
return Ok(lisp_directory_file_name(&dir));
}
filename = concat_file_name_lisp(&dir, &filename_no_dir);
match file_symlink_target_lisp(&filename) {
Some(target) => {
filename = lisp_files_splice_dirname_file(&dir, &target);
}
None => return Ok(filename),
}
}
}
fn file_truename_lisp(
filename: &crate::heap_types::LispString,
default_dir: Option<&crate::heap_types::LispString>,
) -> Result<crate::heap_types::LispString, Flow> {
let default_dir = default_dir
.cloned()
.unwrap_or_else(fallback_root_default_directory);
let mut remaining_links = 100;
let mut prev_dirs = HashMap::new();
file_truename_lisp_inner(filename, &default_dir, &mut remaining_links, &mut prev_dirs)
}
fn clean_path(path: &Path) -> String {
let mut components = Vec::new();
for component in path.components() {
match component {
std::path::Component::CurDir => {} std::path::Component::ParentDir => {
if !components.is_empty() {
components.pop();
}
}
other => components.push(other),
}
}
let result: PathBuf = components.iter().collect();
result.to_string_lossy().into_owned()
}
pub fn file_name_directory(filename: &str) -> Option<String> {
if filename.ends_with('/') {
return if filename.is_empty() {
None
} else {
Some(filename.to_string())
};
}
filename.rfind('/').map(|pos| filename[..=pos].to_string())
}
pub fn file_name_nondirectory(filename: &str) -> String {
if filename.ends_with('/') {
return String::new();
}
match filename.rfind('/') {
Some(pos) => filename[pos + 1..].to_string(),
None => filename.to_string(),
}
}
pub fn file_name_as_directory(filename: &str) -> String {
if filename.is_empty() {
"./".to_string()
} else if filename.ends_with('/') {
filename.to_string()
} else {
format!("{filename}/")
}
}
pub fn directory_file_name(filename: &str) -> String {
if filename.is_empty() {
return String::new();
}
if filename.bytes().all(|b| b == b'/') {
return if filename.len() == 2 {
"//".to_string()
} else {
"/".to_string()
};
}
filename.trim_end_matches('/').to_string()
}
pub fn file_name_concat(parts: &[&str]) -> String {
let mut iter = parts.iter().copied().filter(|s| !s.is_empty());
let Some(first) = iter.next() else {
return String::new();
};
let mut out = first.to_string();
for part in iter {
if !out.ends_with('/') {
out.push('/');
}
out.push_str(part);
}
out
}
pub fn file_name_absolute_p(filename: &str) -> bool {
filename.starts_with('/') || filename.starts_with('~')
}
pub fn directory_name_p(name: &str) -> bool {
name.ends_with('/')
}
static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
static DEFAULT_FILE_MODE_MASK: AtomicU32 = AtomicU32::new(0o022);
static DEFAULT_FILE_MODE_MASK_INIT: Once = Once::new();
fn init_default_file_mode_mask() {
DEFAULT_FILE_MODE_MASK_INIT.call_once(|| {
#[cfg(unix)]
unsafe {
let old = libc::umask(0);
libc::umask(old);
DEFAULT_FILE_MODE_MASK.store(old as u32, Ordering::Relaxed);
}
});
}
fn env_name_char(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
fn trim_embedded_absfilename(path: String) -> String {
let mut current = path;
loop {
let bytes = current.as_bytes();
let mut cut_at = None;
let mut i = 1usize;
while i < bytes.len() {
if bytes[i - 1] == b'/' && (bytes[i] == b'/' || bytes[i] == b'~') {
cut_at = Some(i);
break;
}
i += 1;
}
if let Some(idx) = cut_at {
current = current[idx..].to_string();
} else {
return current;
}
}
}
#[cfg(unix)]
fn trim_embedded_absfilename_bytes(path: Vec<u8>) -> Vec<u8> {
let mut current = path;
loop {
let mut cut_at = None;
let mut i = 1usize;
while i < current.len() {
if current[i - 1] == b'/' && (current[i] == b'/' || current[i] == b'~') {
cut_at = Some(i);
break;
}
i += 1;
}
if let Some(idx) = cut_at {
current = current[idx..].to_vec();
} else {
return current;
}
}
}
pub fn substitute_in_file_name(filename: &str) -> String {
let bytes = filename.as_bytes();
let mut out = String::with_capacity(filename.len());
let mut i = 0usize;
while i < bytes.len() {
if bytes[i] != b'$' {
let ch = filename[i..]
.chars()
.next()
.expect("index points at valid char boundary");
out.push(ch);
i += ch.len_utf8();
continue;
}
if i + 1 >= bytes.len() {
out.push('$');
i += 1;
continue;
}
match bytes[i + 1] {
b'$' => {
out.push('$');
i += 2;
}
b'{' => {
if let Some(rel_end) = bytes[i + 2..].iter().position(|&b| b == b'}') {
let end = i + 2 + rel_end;
let var = &filename[i + 2..end];
if let Ok(value) = std::env::var(var) {
out.push_str(&value);
} else {
out.push_str(&filename[i..=end]);
}
i = end + 1;
} else {
out.push('$');
i += 1;
}
}
next if env_name_char(next) => {
let mut end = i + 1;
while end < bytes.len() && env_name_char(bytes[end]) {
end += 1;
}
let var = &filename[i + 1..end];
if let Ok(value) = std::env::var(var) {
out.push_str(&value);
} else {
out.push_str(&filename[i..end]);
}
i = end;
}
_ => {
out.push('$');
i += 1;
}
}
}
trim_embedded_absfilename(out)
}
pub(crate) fn substitute_in_file_name_lisp(
filename: &crate::heap_types::LispString,
) -> crate::heap_types::LispString {
#[cfg(unix)]
{
let bytes = filename.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0usize;
while i < bytes.len() {
if bytes[i] != b'$' {
out.push(bytes[i]);
i += 1;
continue;
}
if i + 1 >= bytes.len() {
out.push(b'$');
i += 1;
continue;
}
match bytes[i + 1] {
b'$' => {
out.push(b'$');
i += 2;
}
b'{' => {
if let Some(rel_end) = bytes[i + 2..].iter().position(|&b| b == b'}') {
let end = i + 2 + rel_end;
let var = String::from_utf8_lossy(&bytes[i + 2..end]);
if let Some(value) = std::env::var_os(var.as_ref()) {
out.extend_from_slice(value.as_bytes());
} else {
out.extend_from_slice(&bytes[i..=end]);
}
i = end + 1;
} else {
out.push(b'$');
i += 1;
}
}
next if env_name_char(next) => {
let mut end = i + 1;
while end < bytes.len() && env_name_char(bytes[end]) {
end += 1;
}
let var = String::from_utf8_lossy(&bytes[i + 1..end]);
if let Some(value) = std::env::var_os(var.as_ref()) {
out.extend_from_slice(value.as_bytes());
} else {
out.extend_from_slice(&bytes[i..end]);
}
i = end;
}
_ => {
out.push(b'$');
i += 1;
}
}
}
crate::heap_types::LispString::from_unibyte(trim_embedded_absfilename_bytes(out))
}
#[cfg(not(unix))]
{
let substituted = substitute_in_file_name(
&crate::emacs_core::builtins::runtime_string_from_lisp_string(filename),
);
crate::emacs_core::builtins::runtime_string_to_lisp_string(
&substituted,
!substituted.is_ascii(),
)
}
}
pub fn file_exists_p(filename: &str) -> bool {
Path::new(filename).exists()
}
pub fn file_readable_p(filename: &str) -> bool {
fs::File::open(filename).is_ok()
}
pub fn file_writable_p(filename: &str) -> bool {
let path = Path::new(filename);
if path.exists() {
fs::OpenOptions::new().write(true).open(filename).is_ok()
} else {
match path.parent() {
Some(parent) if parent.exists() => {
let test_path = parent.join(".neovm_write_test");
match fs::File::create(&test_path) {
Ok(_) => {
let _ = fs::remove_file(&test_path);
true
}
Err(_) => false,
}
}
_ => false,
}
}
}
pub fn file_accessible_directory_p(filename: &str) -> bool {
let path = Path::new(filename);
if !path.is_dir() {
return false;
}
#[cfg(unix)]
{
let Ok(c_path) = CString::new(filename) else {
return false;
};
let mode = libc::R_OK | libc::X_OK;
unsafe { libc::access(c_path.as_ptr(), mode) == 0 }
}
#[cfg(not(unix))]
{
return fs::read_dir(path).is_ok();
}
}
pub fn file_executable_p(filename: &str) -> bool {
#[cfg(unix)]
{
let Ok(c_path) = CString::new(filename) else {
return false;
};
unsafe { libc::access(c_path.as_ptr(), libc::X_OK) == 0 }
}
#[cfg(not(unix))]
{
return Path::new(filename).exists();
}
}
pub fn file_locked_p(_filename: &str) -> bool {
false
}
fn file_system_info_path(path: &Path) -> std::io::Result<(i64, i64, i64)> {
#[cfg(unix)]
{
fn saturating_i64(v: u128) -> i64 {
if v > i64::MAX as u128 {
i64::MAX
} else {
v as i64
}
}
let c_path = path_to_cstring(path).map_err(|_| {
std::io::Error::new(ErrorKind::InvalidInput, "embedded NUL in file name")
})?;
let mut stats: libc::statvfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statvfs(c_path.as_ptr(), &mut stats as *mut libc::statvfs) } != 0 {
return Err(std::io::Error::last_os_error());
}
let block_size = if stats.f_frsize > 0 {
stats.f_frsize as u128
} else {
stats.f_bsize as u128
};
let total = (stats.f_blocks as u128) * block_size;
let free = (stats.f_bfree as u128) * block_size;
let available = (stats.f_bavail as u128) * block_size;
Ok((
saturating_i64(total),
saturating_i64(free),
saturating_i64(available),
))
}
#[cfg(not(unix))]
{
let _ = path;
Ok((0, 0, 0))
}
}
pub fn file_directory_p(filename: &str) -> bool {
Path::new(filename).is_dir()
}
pub fn file_regular_p(filename: &str) -> bool {
Path::new(filename).is_file()
}
pub fn file_symlink_p(filename: &str) -> bool {
match fs::symlink_metadata(filename) {
Ok(meta) => meta.file_type().is_symlink(),
Err(_) => false,
}
}
pub fn file_symlink_target(filename: &str) -> Option<String> {
let meta = fs::symlink_metadata(filename).ok()?;
if !meta.file_type().is_symlink() {
return None;
}
fs::read_link(filename)
.ok()
.map(|p| p.to_string_lossy().into_owned())
}
pub fn file_symlink_target_lisp(
filename: &crate::heap_types::LispString,
) -> Option<crate::heap_types::LispString> {
let path = lisp_file_name_to_path_buf(filename);
let meta = fs::symlink_metadata(&path).ok()?;
if !meta.file_type().is_symlink() {
return None;
}
fs::read_link(&path)
.ok()
.map(|target| path_to_lisp_file_name(&target))
}
pub fn file_name_case_insensitive_p(filename: &str) -> bool {
let mut probe = PathBuf::from(filename);
while !probe.exists() {
if !probe.pop() || probe.as_os_str().is_empty() {
return false;
}
}
#[cfg(windows)]
{
true
}
#[cfg(not(windows))]
{
false
}
}
pub fn file_newer_than_file_p(file1: &str, file2: &str) -> bool {
let meta1 = match fs::metadata(file1) {
Ok(meta) => meta,
Err(_) => return false,
};
let meta2 = match fs::metadata(file2) {
Ok(meta) => meta,
Err(_) => return true,
};
let mtime1 = match meta1.modified() {
Ok(time) => time,
Err(_) => return false,
};
let mtime2 = match meta2.modified() {
Ok(time) => time,
Err(_) => return true,
};
mtime1 > mtime2
}
fn file_newer_than_file_path(file1: &Path, file2: &Path) -> bool {
let meta1 = match fs::metadata(file1) {
Ok(meta) => meta,
Err(_) => return false,
};
let meta2 = match fs::metadata(file2) {
Ok(meta) => meta,
Err(_) => return true,
};
let mtime1 = match meta1.modified() {
Ok(time) => time,
Err(_) => return false,
};
let mtime2 = match meta2.modified() {
Ok(time) => time,
Err(_) => return true,
};
mtime1 > mtime2
}
pub fn read_file_contents(filename: &str) -> std::io::Result<String> {
fs::read_to_string(filename)
}
pub fn write_string_to_file(content: &str, filename: &str, append: bool) -> std::io::Result<()> {
let mode = if append {
FileWriteMode::Append
} else {
FileWriteMode::Truncate
};
let file = write_bytes_to_file_with_mode(content.as_bytes(), Path::new(filename), mode)?;
drop(file);
Ok(())
}
enum FileWriteMode {
Truncate,
Append,
Seek(u64),
}
fn write_bytes_to_file_with_mode(
content: &[u8],
filename: &Path,
mode: FileWriteMode,
) -> std::io::Result<fs::File> {
let mut options = fs::OpenOptions::new();
options.write(true).create(true);
match mode {
FileWriteMode::Truncate => {
options.truncate(true);
}
FileWriteMode::Append => {
options.append(true);
}
FileWriteMode::Seek(_) => {}
}
let mut file = options.open(filename)?;
if let FileWriteMode::Seek(offset) = mode {
file.seek(SeekFrom::Start(offset))?;
}
file.write_all(content)?;
Ok(file)
}
#[cfg(unix)]
fn read_directory_names(dir: &str) -> Result<Vec<String>, DirectoryFilesError> {
let dir_cstr = CString::new(dir).map_err(|_| DirectoryFilesError::Io {
action: "Opening directory",
err: std::io::Error::new(ErrorKind::InvalidInput, "path contains interior NUL"),
})?;
let dirp = unsafe { libc::opendir(dir_cstr.as_ptr()) };
if dirp.is_null() {
return Err(DirectoryFilesError::Io {
action: "Opening directory",
err: std::io::Error::last_os_error(),
});
}
let mut names = Vec::new();
loop {
let entry = unsafe { libc::readdir(dirp) };
if entry.is_null() {
break;
}
let raw_name = unsafe { CStr::from_ptr((*entry).d_name.as_ptr()) };
names.push(raw_name.to_string_lossy().into_owned());
}
let _ = unsafe { libc::closedir(dirp) };
Ok(names)
}
#[cfg(not(unix))]
fn read_directory_names(dir: &str) -> Result<Vec<String>, DirectoryFilesError> {
let entries = fs::read_dir(dir).map_err(|e| DirectoryFilesError::Io {
action: "Opening directory",
err: e,
})?;
let mut names = vec![".".to_string(), "..".to_string()];
for entry in entries {
let entry = entry.map_err(|e| DirectoryFilesError::Io {
action: "Reading directory entry",
err: e,
})?;
names.push(entry.file_name().to_string_lossy().into_owned());
}
Ok(names)
}
#[derive(Debug)]
enum DirectoryFilesError {
Io {
action: &'static str,
err: std::io::Error,
},
InvalidRegexp(String),
}
fn directory_files(
dir: &str,
full: bool,
match_regex: Option<&str>,
nosort: bool,
count: Option<usize>,
) -> Result<Vec<String>, DirectoryFilesError> {
if count == Some(0) {
return Ok(Vec::new());
}
let names = read_directory_names(dir)?;
let mut result = VecDeque::new();
let mut remaining = count.unwrap_or(usize::MAX);
let dir_with_slash = if dir.ends_with('/') {
dir.to_string()
} else {
format!("{dir}/")
};
for name in names {
if let Some(pattern) = match_regex {
let mut throwaway = None;
let matched = super::regex::string_match_full_with_case_fold(
pattern,
&name,
0,
false,
&mut throwaway,
)
.map_err(|msg| {
DirectoryFilesError::InvalidRegexp(format!(
"Invalid regexp \"{}\": {}",
pattern, msg
))
})?;
if matched.is_none() {
continue;
}
}
if full {
result.push_front(format!("{dir_with_slash}{name}"));
} else {
result.push_front(name);
}
if remaining != usize::MAX {
remaining -= 1;
if remaining == 0 {
break;
}
}
}
let mut result: Vec<String> = result.into_iter().collect();
if !nosort {
result.sort();
}
Ok(result)
}
pub fn make_directory(dir: &str, parents: bool) -> std::io::Result<()> {
if parents {
fs::create_dir_all(dir)
} else {
fs::create_dir(dir)
}
}
pub fn delete_file(filename: &str) -> std::io::Result<()> {
fs::remove_file(filename)
}
pub fn rename_file(from: &str, to: &str) -> std::io::Result<()> {
fs::rename(from, to)
}
pub fn copy_file(from: &str, to: &str) -> std::io::Result<()> {
fs::copy(from, to).map(|_| ())
}
pub fn add_name_to_file(oldname: &str, newname: &str) -> std::io::Result<()> {
fs::hard_link(oldname, newname)
}
#[derive(Debug, Clone)]
pub struct FileAttributes {
pub size: u64,
pub nlinks: u64,
pub is_dir: bool,
pub is_symlink: bool,
pub modified: Option<f64>, pub modes: u32,
}
pub fn file_attributes(filename: &str) -> Option<FileAttributes> {
let meta = fs::metadata(filename).ok()?;
let symlink_meta = fs::symlink_metadata(filename).ok();
let modified = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs_f64());
#[cfg(unix)]
let modes = {
use std::os::unix::fs::PermissionsExt;
meta.permissions().mode()
};
#[cfg(not(unix))]
let modes = if meta.permissions().readonly() {
0o444
} else {
0o644
};
#[cfg(unix)]
let nlinks = {
use std::os::unix::fs::MetadataExt;
meta.nlink()
};
#[cfg(not(unix))]
let nlinks = 1;
Some(FileAttributes {
size: meta.len(),
nlinks,
is_dir: meta.is_dir(),
is_symlink: symlink_meta.is_some_and(|m| m.file_type().is_symlink()),
modified,
modes,
})
}
fn file_modes(filename: &str) -> Option<u32> {
let meta = fs::symlink_metadata(filename).ok()?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
Some(meta.permissions().mode() & 0o7777)
}
#[cfg(not(unix))]
{
Some(if meta.permissions().readonly() {
0o444
} else {
0o644
})
}
}
fn expect_args(name: &str, args: &[Value], n: usize) -> Result<(), Flow> {
if args.len() != n {
Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol(name), Value::fixnum(args.len() as i64)],
))
} else {
Ok(())
}
}
fn expect_min_args(name: &str, args: &[Value], min: usize) -> Result<(), Flow> {
if args.len() < min {
Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol(name), Value::fixnum(args.len() as i64)],
))
} else {
Ok(())
}
}
fn expect_max_args(name: &str, args: &[Value], max: usize) -> Result<(), Flow> {
if args.len() > max {
Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol(name), Value::fixnum(args.len() as i64)],
))
} else {
Ok(())
}
}
fn fileio_owned_runtime_string(value: Value) -> String {
value
.as_runtime_string_owned()
.expect("ValueKind::String must carry LispString payload")
}
fn fileio_owned_runtime_string_opt(value: &Value) -> Option<String> {
value.as_runtime_string_owned()
}
fn expect_string(value: &Value) -> Result<String, Flow> {
match value.kind() {
ValueKind::String => Ok(fileio_owned_runtime_string(*value)),
ValueKind::Symbol(id) => Ok(resolve_sym(id).to_owned()),
ValueKind::Nil => Ok("nil".to_string()),
ValueKind::T => Ok("t".to_string()),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), *value],
)),
}
}
fn expect_string_strict(value: &Value) -> Result<String, Flow> {
match value.kind() {
ValueKind::String => Ok(fileio_owned_runtime_string(*value)),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), *value],
)),
}
}
fn expect_lisp_string_strict(value: &Value) -> Result<crate::heap_types::LispString, Flow> {
match value.kind() {
ValueKind::String => Ok(value
.as_lisp_string()
.expect("ValueKind::String must carry LispString payload")
.clone()),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), *value],
)),
}
}
fn file_name_runtime_result_value(text: &str, multibyte: bool) -> Value {
Value::heap_string(crate::emacs_core::builtins::runtime_string_to_lisp_string(
text, multibyte,
))
}
fn file_name_lisp_from_bytes(bytes: Vec<u8>, multibyte: bool) -> crate::heap_types::LispString {
if multibyte {
crate::heap_types::LispString::from_emacs_bytes(bytes)
} else {
crate::heap_types::LispString::from_unibyte(bytes)
}
}
fn fallback_root_default_directory() -> crate::heap_types::LispString {
crate::heap_types::LispString::from_utf8("/")
}
fn expand_file_name_result_multibyte(
name: &crate::heap_types::LispString,
default_directory: &crate::heap_types::LispString,
) -> bool {
let mut multibyte = name.is_multibyte();
let defdir_multibyte = default_directory.is_multibyte();
if multibyte != defdir_multibyte {
if multibyte {
if name.is_ascii() || !default_directory.is_ascii() {
multibyte = false;
}
} else if name.is_ascii() {
multibyte = true;
}
}
multibyte
}
fn file_name_concat_result_multibyte(parts: &[&crate::heap_types::LispString]) -> bool {
let any_multibyte = parts.iter().any(|part| part.is_multibyte());
let any_unibyte_non_ascii = parts
.iter()
.any(|part| !part.is_multibyte() && !part.is_ascii());
any_multibyte && !any_unibyte_non_ascii
}
fn concat_file_name_lisp(
left: &crate::heap_types::LispString,
right: &crate::heap_types::LispString,
) -> crate::heap_types::LispString {
let mut bytes = Vec::with_capacity(left.as_bytes().len() + right.as_bytes().len());
bytes.extend_from_slice(left.as_bytes());
bytes.extend_from_slice(right.as_bytes());
file_name_lisp_from_bytes(bytes, file_name_concat_result_multibyte(&[left, right]))
}
fn lisp_file_name_directory(
filename: &crate::heap_types::LispString,
) -> Option<crate::heap_types::LispString> {
let bytes = filename.as_bytes();
if bytes.ends_with(b"/") {
return (!bytes.is_empty()).then(|| filename.clone());
}
bytes
.iter()
.rposition(|&byte| byte == b'/')
.map(|pos| file_name_lisp_from_bytes(bytes[..=pos].to_vec(), filename.is_multibyte()))
}
fn lisp_file_name_nondirectory(
filename: &crate::heap_types::LispString,
) -> crate::heap_types::LispString {
let bytes = filename.as_bytes();
if bytes.ends_with(b"/") {
return file_name_lisp_from_bytes(Vec::new(), filename.is_multibyte());
}
match bytes.iter().rposition(|&byte| byte == b'/') {
Some(pos) => file_name_lisp_from_bytes(bytes[pos + 1..].to_vec(), filename.is_multibyte()),
None => filename.clone(),
}
}
fn lisp_file_name_as_directory(
filename: &crate::heap_types::LispString,
) -> crate::heap_types::LispString {
if filename.as_bytes().is_empty() {
return file_name_lisp_from_bytes(b"./".to_vec(), filename.is_multibyte());
}
if filename.as_bytes().ends_with(b"/") {
return filename.clone();
}
let mut bytes = filename.as_bytes().to_vec();
bytes.push(b'/');
file_name_lisp_from_bytes(bytes, filename.is_multibyte())
}
fn lisp_directory_file_name(
filename: &crate::heap_types::LispString,
) -> crate::heap_types::LispString {
let bytes = filename.as_bytes();
if bytes.is_empty() {
return filename.clone();
}
if bytes.iter().all(|&byte| byte == b'/') {
return if bytes.len() == 2 {
filename.clone()
} else {
file_name_lisp_from_bytes(vec![b'/'], filename.is_multibyte())
};
}
let trimmed_len = bytes
.iter()
.rposition(|&byte| byte != b'/')
.map_or(0, |pos| pos + 1);
file_name_lisp_from_bytes(bytes[..trimmed_len].to_vec(), filename.is_multibyte())
}
fn expand_file_name_lisp(
name: &crate::heap_types::LispString,
default_directory: Option<&crate::heap_types::LispString>,
) -> crate::heap_types::LispString {
let name_runtime = crate::emacs_core::builtins::runtime_string_from_lisp_string(name);
let default_directory = default_directory
.cloned()
.unwrap_or_else(fallback_root_default_directory);
let default_runtime =
crate::emacs_core::builtins::runtime_string_from_lisp_string(&default_directory);
let result = expand_file_name(&name_runtime, Some(&default_runtime));
crate::emacs_core::builtins::runtime_string_to_lisp_string(
&result,
expand_file_name_result_multibyte(name, &default_directory),
)
}
fn lisp_file_name_absolute_system_p(filename: &crate::heap_types::LispString) -> bool {
filename.as_bytes().first() == Some(&b'/')
}
fn lisp_string_runtime_eq(
left: &crate::heap_types::LispString,
right: &crate::heap_types::LispString,
) -> bool {
left.as_bytes() == right.as_bytes()
}
fn lisp_file_name_is_ascii_text(filename: &crate::heap_types::LispString, text: &[u8]) -> bool {
filename.as_bytes() == text
}
fn lisp_directory_name_p(filename: &crate::heap_types::LispString) -> bool {
filename.as_bytes().last() == Some(&b'/')
}
fn lisp_string_strip_ascii_prefix(
value: &crate::heap_types::LispString,
prefix: &[u8],
) -> Option<crate::heap_types::LispString> {
let rest = value.as_bytes().strip_prefix(prefix)?;
Some(if value.is_multibyte() {
crate::heap_types::LispString::from_emacs_bytes(rest.to_vec())
} else {
crate::heap_types::LispString::from_unibyte(rest.to_vec())
})
}
fn wrap_ascii_around_lisp_string(
value: &crate::heap_types::LispString,
prefix: &[u8],
suffix: &[u8],
) -> crate::heap_types::LispString {
let mut bytes = Vec::with_capacity(prefix.len() + value.as_bytes().len() + suffix.len());
bytes.extend_from_slice(prefix);
bytes.extend_from_slice(value.as_bytes());
bytes.extend_from_slice(suffix);
file_name_lisp_from_bytes(bytes, value.is_multibyte())
}
fn expand_cp_target_lisp_for_eval(
eval: &Context,
file: &crate::heap_types::LispString,
newname: &crate::heap_types::LispString,
) -> crate::heap_types::LispString {
if lisp_directory_name_p(newname) {
let resolved_dir = resolve_filename_lisp_for_eval(eval, newname);
expand_file_name_lisp(&lisp_file_name_nondirectory(file), Some(&resolved_dir))
} else {
resolve_filename_lisp_for_eval(eval, newname)
}
}
fn expand_and_dir_to_file_lisp_for_eval(
eval: &Context,
filename: &crate::heap_types::LispString,
) -> crate::heap_types::LispString {
let absname = resolve_filename_lisp_for_eval(eval, filename);
if absname.sbytes() > 1 && lisp_directory_name_p(&absname) {
lisp_directory_file_name(&absname)
} else {
absname
}
}
fn lisp_files_splice_dirname_file(
dirname: &crate::heap_types::LispString,
file: &crate::heap_types::LispString,
) -> crate::heap_types::LispString {
if lisp_file_name_absolute_system_p(file) {
file.clone()
} else {
concat_file_name_lisp(dirname, file)
}
}
fn expect_temp_prefix(value: &Value) -> Result<String, Flow> {
match value.kind() {
ValueKind::String => Ok(fileio_owned_runtime_string(*value)),
ValueKind::Nil | ValueKind::Cons | ValueKind::Veclike(VecLikeType::Vector) => Err(signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), *value],
)),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("sequencep"), *value],
)),
}
}
fn expect_fixnum(value: &Value) -> Result<i64, Flow> {
match value.kind() {
ValueKind::Fixnum(n) => Ok(n),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("fixnump"), *value],
)),
}
}
fn normalize_secs_nanos(mut secs: i64, mut nanos: i64) -> (i64, i64) {
if nanos >= 1_000_000_000 {
secs += nanos / 1_000_000_000;
nanos %= 1_000_000_000;
} else if nanos < 0 {
let borrow = ((-nanos) + 999_999_999) / 1_000_000_000;
secs -= borrow;
nanos += borrow * 1_000_000_000;
}
(secs, nanos)
}
fn parse_timestamp_arg(value: &Value) -> Result<(i64, i64), Flow> {
match value.kind() {
ValueKind::Fixnum(n) => Ok((n, 0)),
ValueKind::Float => {
let f = value.as_float().unwrap();
let secs = f.floor() as i64;
let nanos = ((f - f.floor()) * 1_000_000_000.0).round() as i64;
Ok(normalize_secs_nanos(secs, nanos))
}
ValueKind::Cons => {
let items = list_to_vec(value).ok_or_else(|| {
signal("wrong-type-argument", vec![Value::symbol("listp"), *value])
})?;
if items.len() < 2 {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("listp"), *value],
));
}
let high = items[0].as_int().ok_or_else(|| {
signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), items[0]],
)
})?;
let low = items[1].as_int().ok_or_else(|| {
signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), items[1]],
)
})?;
let usec = if items.len() > 2 {
items[2].as_int().unwrap_or(0)
} else {
0
};
let secs = high * 65_536 + low;
let nanos = usec * 1_000;
Ok(normalize_secs_nanos(secs, nanos))
}
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("numberp"), *value],
)),
}
}
fn validate_file_truename_counter(counter: &Value) -> Result<(), Flow> {
if counter.is_nil() {
return Ok(());
}
if !counter.is_list() {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("listp"), *counter],
));
}
if counter.is_cons() {
let first = counter.cons_car();
if !(first.is_number() || first.as_char().is_some()) {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("number-or-marker-p"), first],
));
}
}
Ok(())
}
fn temporary_file_directory_for_eval(eval: &Context) -> Option<String> {
let val = eval.obarray.symbol_value("temporary-file-directory")?;
fileio_owned_runtime_string_opt(val)
}
fn make_temp_file_impl(
temp_dir: &str,
prefix: &str,
dir_flag: bool,
suffix: &str,
text: Option<&str>,
) -> Result<String, Flow> {
let base = PathBuf::from(temp_dir);
for _ in 0..256 {
let nonce = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let candidate = base.join(format!("{prefix}{now:x}{nonce:x}{suffix}"));
let candidate_str = candidate.to_string_lossy().into_owned();
if dir_flag {
match fs::create_dir(&candidate) {
Ok(()) => {
if let Some(contents) = text {
let mut file = fs::OpenOptions::new()
.write(true)
.open(&candidate)
.map_err(|err| {
signal_file_io_path(err, "Writing to", &candidate_str)
})?;
file.write_all(contents.as_bytes()).map_err(|err| {
signal_file_io_path(err, "Writing to", &candidate_str)
})?;
}
return Ok(candidate_str);
}
Err(err) if err.kind() == ErrorKind::AlreadyExists => continue,
Err(err) => {
return Err(signal_file_io_path(
err,
"Creating directory",
&candidate_str,
));
}
}
} else {
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&candidate)
{
Ok(mut file) => {
if let Some(contents) = text {
file.write_all(contents.as_bytes()).map_err(|err| {
signal_file_io_path(err, "Writing to", &candidate_str)
})?;
}
return Ok(candidate_str);
}
Err(err) if err.kind() == ErrorKind::AlreadyExists => continue,
Err(err) => return Err(signal_file_io_path(err, "Creating file", &candidate_str)),
}
}
}
Err(signal(
"file-error",
vec![Value::string("Cannot create temporary file")],
))
}
fn split_nearby_temp_prefix(prefix: &str) -> Option<(String, String)> {
let path = Path::new(prefix);
if !path.is_absolute() {
return None;
}
let file_name = path.file_name()?.to_string_lossy().into_owned();
if file_name.is_empty() {
return None;
}
let parent = path.parent()?;
if parent.as_os_str().is_empty() || parent == Path::new(".") {
return None;
}
Some((parent.to_string_lossy().into_owned(), file_name))
}
fn make_temp_name_suffix() -> String {
const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
let nonce = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
let mut value = now ^ nonce.rotate_left(7);
let mut out = [b'a'; 6];
for slot in &mut out {
let idx = (value % ALPHABET.len() as u64) as usize;
*slot = ALPHABET[idx];
value = value / ALPHABET.len() as u64 + 1;
}
String::from_utf8_lossy(&out).into_owned()
}
pub(crate) fn builtin_expand_file_name(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "expand-file-name", &args)? {
return Ok(result);
}
expect_min_args("expand-file-name", &args, 1)?;
if args.len() > 2 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("expand-file-name"),
Value::fixnum(args.len() as i64),
],
));
}
let name_lisp = expect_lisp_string_strict(&args[0])?;
let name = crate::emacs_core::builtins::runtime_string_from_lisp_string(&name_lisp);
let default_dir_lisp = if let Some(arg) = args.get(1) {
match arg.kind() {
ValueKind::Nil => default_directory_lisp_for_eval(eval)
.unwrap_or_else(fallback_root_default_directory),
ValueKind::String => expect_lisp_string_strict(arg)?,
_ => fallback_root_default_directory(),
}
} else {
default_directory_lisp_for_eval(eval).unwrap_or_else(fallback_root_default_directory)
};
let default_dir =
crate::emacs_core::builtins::runtime_string_from_lisp_string(&default_dir_lisp);
let result = expand_file_name(&name, Some(&default_dir));
let result_multibyte = expand_file_name_result_multibyte(&name_lisp, &default_dir_lisp);
Ok(file_name_runtime_result_value(&result, result_multibyte))
}
pub(crate) fn builtin_make_temp_name(args: Vec<Value>) -> EvalResult {
expect_args("make-temp-name", &args, 1)?;
let prefix = expect_string_strict(&args[0])?;
Ok(Value::string(format!(
"{prefix}{}",
make_temp_name_suffix()
)))
}
pub(crate) fn builtin_next_read_file_uses_dialog_p(args: Vec<Value>) -> EvalResult {
expect_args("next-read-file-uses-dialog-p", &args, 0)?;
Ok(Value::NIL)
}
pub(crate) fn builtin_unhandled_file_name_directory(args: Vec<Value>) -> EvalResult {
expect_args("unhandled-file-name-directory", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
Ok(file_name_runtime_result_value(
&file_name_as_directory(&filename),
args[0].string_is_multibyte(),
))
}
pub(crate) fn builtin_get_truename_buffer(args: Vec<Value>) -> EvalResult {
expect_args("get-truename-buffer", &args, 1)?;
let _filename = &args[0];
Ok(Value::NIL)
}
pub(crate) fn builtin_make_temp_file(eval: &mut Context, args: Vec<Value>) -> EvalResult {
expect_min_args("make-temp-file", &args, 1)?;
if args.len() > 4 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("make-temp-file"),
Value::fixnum(args.len() as i64),
],
));
}
let prefix = expect_temp_prefix(&args[0])?;
let dir_flag = args.get(1).is_some_and(|value| value.is_truthy());
let suffix = match args.get(2) {
None => String::new(),
Some(v) if v.is_nil() => String::new(),
Some(value) => expect_string_strict(value)?,
};
let text = match args.get(3) {
None => None,
Some(v) if v.is_nil() => None,
Some(v) if v.is_string() => Some(fileio_owned_runtime_string(*v)),
Some(_) => None,
};
let temp_dir = temporary_file_directory_for_eval(eval)
.unwrap_or_else(|| std::env::temp_dir().to_string_lossy().into_owned());
let path = make_temp_file_impl(&temp_dir, &prefix, dir_flag, &suffix, text.as_deref())?;
Ok(Value::string(path))
}
pub(crate) fn builtin_make_nearby_temp_file(eval: &mut Context, args: Vec<Value>) -> EvalResult {
expect_min_args("make-nearby-temp-file", &args, 1)?;
if args.len() > 3 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("make-nearby-temp-file"),
Value::fixnum(args.len() as i64),
],
));
}
let prefix = expect_temp_prefix(&args[0])?;
let dir_flag = args.get(1).is_some_and(|value| value.is_truthy());
let suffix = match args.get(2) {
None => String::new(),
Some(v) if v.is_nil() => String::new(),
Some(value) => expect_string_strict(value)?,
};
let fallback_temp_dir = temporary_file_directory_for_eval(eval)
.unwrap_or_else(|| std::env::temp_dir().to_string_lossy().into_owned());
let (temp_dir, file_prefix) =
split_nearby_temp_prefix(&prefix).unwrap_or_else(|| (fallback_temp_dir, prefix.clone()));
let path = make_temp_file_impl(&temp_dir, &file_prefix, dir_flag, &suffix, None)?;
Ok(Value::string(path))
}
pub(crate) fn builtin_file_truename(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-truename", &args)? {
return Ok(result);
}
expect_min_args("file-truename", &args, 1)?;
if args.len() > 3 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("file-truename"),
Value::fixnum(args.len() as i64),
],
));
}
let filename = expect_lisp_string_strict(&args[0])?;
if let Some(counter) = args.get(1) {
validate_file_truename_counter(counter)?;
}
let default_dir = default_directory_lisp_for_eval(eval);
Ok(Value::heap_string(file_truename_lisp(
&filename,
default_dir.as_ref(),
)?))
}
pub(crate) fn builtin_file_name_directory(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-name-directory", &args)? {
return Ok(result);
}
expect_args("file-name-directory", &args, 1)?;
let filename = expect_lisp_string_strict(&args[0])?;
match lisp_file_name_directory(&filename) {
Some(dir) => Ok(Value::heap_string(dir)),
None => Ok(Value::NIL),
}
}
pub(crate) fn builtin_file_name_nondirectory(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-name-nondirectory", &args)? {
return Ok(result);
}
expect_args("file-name-nondirectory", &args, 1)?;
let filename = expect_lisp_string_strict(&args[0])?;
Ok(Value::heap_string(lisp_file_name_nondirectory(&filename)))
}
pub(crate) fn builtin_file_name_as_directory(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-name-as-directory", &args)? {
return Ok(result);
}
expect_args("file-name-as-directory", &args, 1)?;
let filename = expect_lisp_string_strict(&args[0])?;
Ok(Value::heap_string(lisp_file_name_as_directory(&filename)))
}
pub(crate) fn builtin_directory_file_name(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "directory-file-name", &args)? {
return Ok(result);
}
expect_args("directory-file-name", &args, 1)?;
let filename = expect_lisp_string_strict(&args[0])?;
Ok(Value::heap_string(lisp_directory_file_name(&filename)))
}
pub(crate) fn builtin_file_name_concat(args: Vec<Value>) -> EvalResult {
expect_min_args("file-name-concat", &args, 1)?;
let mut parts = Vec::new();
for value in args {
match value.kind() {
ValueKind::Nil => {}
ValueKind::String => {
let s = fileio_owned_runtime_string(value);
if !s.is_empty() {
parts.push(s);
}
}
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), value],
));
}
}
}
let refs: Vec<&str> = parts.iter().map(String::as_str).collect();
Ok(Value::string(file_name_concat(&refs)))
}
pub(crate) fn builtin_file_name_absolute_p(args: Vec<Value>) -> EvalResult {
expect_args("file-name-absolute-p", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
Ok(Value::bool_val(file_name_absolute_p(&filename)))
}
pub(crate) fn builtin_directory_name_p(args: Vec<Value>) -> EvalResult {
expect_args("directory-name-p", &args, 1)?;
let name = expect_string_strict(&args[0])?;
Ok(Value::bool_val(directory_name_p(&name)))
}
pub(crate) fn builtin_substitute_in_file_name(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "substitute-in-file-name", &args)? {
return Ok(result);
}
expect_args("substitute-in-file-name", &args, 1)?;
match args[0].kind() {
ValueKind::String => Ok(Value::heap_string(substitute_in_file_name_lisp(
args[0]
.as_lisp_string()
.expect("ValueKind::String must carry LispString payload"),
))),
_ => Err(signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), args[0]],
)),
}
}
pub(crate) fn default_directory_in_state(
obarray: &Obarray,
_dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
) -> Option<String> {
if let Some(buf) = buffers.current_buffer() {
if let Some(val) = buf.get_buffer_local("default-directory") {
if val.is_string() {
return fileio_owned_runtime_string_opt(&val);
}
}
}
match obarray.symbol_value("default-directory") {
Some(val) if val.is_string() => fileio_owned_runtime_string_opt(val),
_ => None,
}
}
pub(crate) fn default_directory_lisp_in_state(
obarray: &Obarray,
_dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
) -> Option<crate::heap_types::LispString> {
if let Some(buf) = buffers.current_buffer() {
if let Some(val) = buf.get_buffer_local("default-directory") {
if let Some(string) = val.as_lisp_string() {
return Some(string.clone());
}
}
}
obarray
.symbol_value("default-directory")
.and_then(|val| val.as_lisp_string().cloned())
}
fn default_directory_for_eval(eval: &Context) -> Option<String> {
default_directory_in_state(&eval.obarray, &[], &eval.buffers)
}
fn default_directory_lisp_for_eval(eval: &Context) -> Option<crate::heap_types::LispString> {
default_directory_lisp_in_state(&eval.obarray, &[], &eval.buffers)
}
fn resolve_filename_lisp_in_state(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
filename: &crate::heap_types::LispString,
) -> crate::heap_types::LispString {
if lisp_file_name_absolute_system_p(filename) {
return filename.clone();
}
let default_dir = default_directory_lisp_in_state(obarray, dynamic, buffers)
.unwrap_or_else(fallback_root_default_directory);
expand_file_name_lisp(filename, Some(&default_dir))
}
fn resolve_filename_lisp_for_eval(
eval: &Context,
filename: &crate::heap_types::LispString,
) -> crate::heap_types::LispString {
resolve_filename_lisp_in_state(&eval.obarray, &[], &eval.buffers, filename)
}
pub(crate) fn resolve_filename_in_state(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
filename: &str,
) -> String {
if filename.is_empty() || Path::new(filename).is_absolute() {
return filename.to_string();
}
let default_dir = default_directory_in_state(obarray, dynamic, buffers);
expand_file_name(filename, default_dir.as_deref())
}
pub(crate) fn resolve_filename_for_eval(eval: &Context, filename: &str) -> String {
resolve_filename_in_state(&eval.obarray, &[], &eval.buffers, filename)
}
fn file_error_symbol(kind: ErrorKind) -> &'static str {
match kind {
ErrorKind::NotFound => "file-missing",
ErrorKind::AlreadyExists => "file-already-exists",
ErrorKind::PermissionDenied => "permission-denied",
_ => "file-error",
}
}
fn signal_file_io_error(err: std::io::Error, context: String) -> Flow {
let symbol = file_error_symbol(err.kind());
signal(symbol, vec![Value::string(format!("{context}: {err}"))])
}
fn signal_file_io_path(err: std::io::Error, action: &str, path: &str) -> Flow {
signal_file_io_error(err, format!("{action} {path}"))
}
fn signal_file_io_paths(err: std::io::Error, action: &str, from: &str, to: &str) -> Flow {
signal_file_io_error(err, format!("{action} {from} to {to}"))
}
fn signal_directory_files_error(err: DirectoryFilesError, dir: &str) -> Flow {
match err {
DirectoryFilesError::Io { action, err } => signal_file_io_path(err, action, dir),
DirectoryFilesError::InvalidRegexp(msg) => {
signal("invalid-regexp", vec![Value::string(msg)])
}
}
}
fn signal_file_action_error(err: std::io::Error, action: &str, path: &str) -> Flow {
signal(
file_error_symbol(err.kind()),
vec![
Value::string(action),
Value::string(err.to_string()),
Value::string(path),
],
)
}
fn signal_file_action_error_value(err: std::io::Error, action: &str, path: Value) -> Flow {
signal(
file_error_symbol(err.kind()),
vec![Value::string(action), Value::string(err.to_string()), path],
)
}
fn signal_file_action_error_pair_values(
err: std::io::Error,
action: &str,
left: Value,
right: Value,
) -> Flow {
signal(
file_error_symbol(err.kind()),
vec![
Value::string(action),
Value::string(err.to_string()),
left,
right,
],
)
}
fn signal_existing_path_value(path: &Path, value: Value) -> Flow {
if fs::symlink_metadata(path)
.map(|metadata| metadata.is_dir())
.unwrap_or(false)
{
signal(
"file-error",
vec![Value::string("File is a directory"), value],
)
} else {
signal(
"file-already-exists",
vec![Value::string("File already exists"), value],
)
}
}
fn maybe_dispatch_resolved_file_handler(
eval: &mut Context,
operation_name: &str,
first_lookup: Option<&crate::heap_types::LispString>,
second_lookup: Option<&crate::heap_types::LispString>,
mut call_args: Vec<Value>,
) -> Result<Option<Value>, Flow> {
let operation_sym = Value::symbol(operation_name);
if let Some(first) = first_lookup {
let handler = find_file_name_handler_lisp(&eval.obarray, first, operation_sym);
if !handler.is_nil() {
call_args.insert(0, operation_sym);
return Ok(Some(eval.funcall_general(handler, call_args)?));
}
}
if let Some(second) = second_lookup {
let handler = find_file_name_handler_lisp(&eval.obarray, second, operation_sym);
if !handler.is_nil() {
call_args.insert(0, operation_sym);
return Ok(Some(eval.funcall_general(handler, call_args)?));
}
}
Ok(None)
}
#[cfg(unix)]
fn path_to_cstring(path: &Path) -> Result<CString, std::ffi::NulError> {
CString::new(path.as_os_str().as_bytes())
}
fn file_exists_path(path: &Path) -> bool {
path.exists()
}
fn file_readable_path(path: &Path) -> bool {
#[cfg(unix)]
{
let Ok(c_path) = path_to_cstring(path) else {
return false;
};
unsafe { libc::access(c_path.as_ptr(), libc::R_OK) == 0 }
}
#[cfg(not(unix))]
{
fs::File::open(path).is_ok()
}
}
fn file_writable_path(path: &Path) -> bool {
#[cfg(unix)]
{
let Ok(c_path) = path_to_cstring(path) else {
return false;
};
if unsafe { libc::access(c_path.as_ptr(), libc::W_OK) == 0 } {
return true;
}
if std::io::Error::last_os_error().kind() != ErrorKind::NotFound {
return false;
}
let Some(parent) = path.parent() else {
return false;
};
let Ok(c_parent) = path_to_cstring(parent) else {
return false;
};
let mode = libc::W_OK | libc::X_OK;
unsafe { libc::access(c_parent.as_ptr(), mode) == 0 }
}
#[cfg(not(unix))]
{
if path.exists() {
fs::OpenOptions::new().write(true).open(path).is_ok()
} else {
match path.parent() {
Some(parent) if parent.exists() => {
let test_path = parent.join(".neovm_write_test");
match fs::File::create(&test_path) {
Ok(_) => {
let _ = fs::remove_file(&test_path);
true
}
Err(_) => false,
}
}
_ => false,
}
}
}
}
fn file_accessible_directory_path(path: &Path) -> bool {
if !path.is_dir() {
return false;
}
#[cfg(unix)]
{
let Ok(c_path) = path_to_cstring(path) else {
return false;
};
let mode = libc::R_OK | libc::X_OK;
unsafe { libc::access(c_path.as_ptr(), mode) == 0 }
}
#[cfg(not(unix))]
{
fs::read_dir(path).is_ok()
}
}
fn file_executable_path(path: &Path) -> bool {
#[cfg(unix)]
{
let Ok(c_path) = path_to_cstring(path) else {
return false;
};
unsafe { libc::access(c_path.as_ptr(), libc::X_OK) == 0 }
}
#[cfg(not(unix))]
{
path.exists()
}
}
fn access_file_path(path: &Path) -> std::io::Result<()> {
#[cfg(unix)]
{
let c_path = path_to_cstring(path).map_err(|err| {
std::io::Error::new(
ErrorKind::InvalidInput,
format!("embedded NUL in file name: {err}"),
)
})?;
if unsafe { libc::access(c_path.as_ptr(), libc::R_OK) == 0 } {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
#[cfg(not(unix))]
{
fs::File::open(path).map(|_| ())
}
}
fn file_directory_path(path: &Path) -> bool {
path.is_dir()
}
fn file_regular_path(path: &Path) -> bool {
path.is_file()
}
fn file_name_case_insensitive_path(path: &Path) -> bool {
let mut probe = path.to_path_buf();
while !probe.exists() {
if !probe.pop() || probe.as_os_str().is_empty() {
return false;
}
}
#[cfg(windows)]
{
true
}
#[cfg(not(windows))]
{
false
}
}
fn file_modes_path(path: &Path, nofollow: bool) -> Option<u32> {
let meta = if nofollow {
fs::symlink_metadata(path).ok()?
} else {
fs::metadata(path).ok()?
};
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
Some(meta.permissions().mode() & 0o7777)
}
#[cfg(not(unix))]
{
Some(if meta.permissions().readonly() {
0o444
} else {
0o644
})
}
}
fn set_file_modes_path(path: &Path, mode: i64, nofollow: bool) -> std::io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if nofollow {
let c_path = path_to_cstring(path).map_err(|err| {
std::io::Error::new(
ErrorKind::InvalidInput,
format!("embedded NUL in file name: {err}"),
)
})?;
let result = unsafe {
libc::fchmodat(
libc::AT_FDCWD,
c_path.as_ptr(),
(mode as libc::mode_t) & 0o7777,
libc::AT_SYMLINK_NOFOLLOW,
)
};
if result == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
} else {
fs::set_permissions(path, fs::Permissions::from_mode((mode as u32) & 0o7777))
}
}
#[cfg(not(unix))]
{
let _ = nofollow;
let mut perms = fs::metadata(path)?.permissions();
let writable = (mode & 0o222) != 0;
perms.set_readonly(!writable);
fs::set_permissions(path, perms)
}
}
fn set_file_times_compat(
filename: &str,
timestamp: Option<(i64, i64)>,
nofollow: bool,
) -> Result<(), Flow> {
#[cfg(unix)]
{
let c_path = CString::new(filename.as_bytes()).map_err(|_| {
signal(
"file-error",
vec![
Value::string("Setting file times"),
Value::string("embedded NUL in file name"),
Value::string(filename),
],
)
})?;
let mut ts = [
libc::timespec {
tv_sec: 0,
tv_nsec: 0,
},
libc::timespec {
tv_sec: 0,
tv_nsec: 0,
},
];
if let Some((secs, nanos)) = timestamp {
ts[0].tv_sec = secs as libc::time_t;
ts[1].tv_sec = secs as libc::time_t;
ts[0].tv_nsec = nanos as libc::c_long;
ts[1].tv_nsec = nanos as libc::c_long;
} else {
ts[0].tv_nsec = libc::UTIME_NOW as libc::c_long;
ts[1].tv_nsec = libc::UTIME_NOW as libc::c_long;
}
let flags = if nofollow {
libc::AT_SYMLINK_NOFOLLOW
} else {
0
};
let result =
unsafe { libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), ts.as_ptr(), flags) };
if result != 0 {
return Err(signal_file_action_error(
std::io::Error::last_os_error(),
"Setting file times",
filename,
));
}
Ok(())
}
#[cfg(not(unix))]
{
let _ = (timestamp, nofollow);
Err(signal(
"file-error",
vec![
Value::string("Setting file times"),
Value::string("set-file-times is unsupported on this platform"),
Value::string(filename),
],
))
}
}
fn set_file_times_path(
path: &Path,
timestamp: Option<(i64, i64)>,
nofollow: bool,
) -> std::io::Result<()> {
#[cfg(unix)]
{
let c_path = path_to_cstring(path).map_err(|_| {
std::io::Error::new(ErrorKind::InvalidInput, "embedded NUL in file name")
})?;
let mut ts = [
libc::timespec {
tv_sec: 0,
tv_nsec: 0,
},
libc::timespec {
tv_sec: 0,
tv_nsec: 0,
},
];
if let Some((secs, nanos)) = timestamp {
ts[0].tv_sec = secs as libc::time_t;
ts[1].tv_sec = secs as libc::time_t;
ts[0].tv_nsec = nanos as libc::c_long;
ts[1].tv_nsec = nanos as libc::c_long;
} else {
ts[0].tv_nsec = libc::UTIME_NOW as libc::c_long;
ts[1].tv_nsec = libc::UTIME_NOW as libc::c_long;
}
let flags = if nofollow {
libc::AT_SYMLINK_NOFOLLOW
} else {
0
};
let result =
unsafe { libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), ts.as_ptr(), flags) };
if result == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
#[cfg(not(unix))]
{
let _ = (path, timestamp, nofollow);
Err(std::io::Error::new(
ErrorKind::Unsupported,
"set-file-times is unsupported on this platform",
))
}
}
fn delete_file_compat(filename: &str) -> Result<(), Flow> {
match delete_file(filename) {
Ok(()) => Ok(()),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
Err(err) => Err(signal_file_io_path(err, "Deleting", filename)),
}
}
fn delete_file_compat_path(path: &Path, path_value: Value) -> Result<(), Flow> {
match fs::remove_file(path) {
Ok(()) => Ok(()),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
Err(err) => Err(signal_file_action_error_value(err, "Deleting", path_value)),
}
}
pub(crate) fn builtin_access_file(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "access-file", &args)? {
return Ok(result);
}
expect_args("access-file", &args, 2)?;
let filename = expect_lisp_string_strict(&args[0])?;
let resolved = resolve_filename_lisp_for_eval(eval, &filename);
let path = lisp_file_name_to_path_buf(&resolved);
let operation = expect_string_strict(&args[1])?;
match access_file_path(&path) {
Ok(_) => Ok(Value::NIL),
Err(err) => Err(signal_file_action_error_value(
err,
&operation,
Value::heap_string(filename),
)),
}
}
pub(crate) fn builtin_file_exists_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-exists-p", &args)? {
return Ok(result);
}
expect_args("file-exists-p", &args, 1)?;
let filename = expect_lisp_string_strict(&args[0])?;
let filename = resolve_filename_lisp_for_eval(eval, &filename);
Ok(Value::bool_val(file_exists_path(
&lisp_file_name_to_path_buf(&filename),
)))
}
pub(crate) fn builtin_file_readable_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-readable-p", &args)? {
return Ok(result);
}
expect_args("file-readable-p", &args, 1)?;
let filename = expect_lisp_string_strict(&args[0])?;
let filename = resolve_filename_lisp_for_eval(eval, &filename);
Ok(Value::bool_val(file_readable_path(
&lisp_file_name_to_path_buf(&filename),
)))
}
pub(crate) fn builtin_file_writable_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-writable-p", &args)? {
return Ok(result);
}
expect_args("file-writable-p", &args, 1)?;
let filename = expect_lisp_string_strict(&args[0])?;
let filename = resolve_filename_lisp_for_eval(eval, &filename);
Ok(Value::bool_val(file_writable_path(
&lisp_file_name_to_path_buf(&filename),
)))
}
pub(crate) fn builtin_file_accessible_directory_p(
eval: &mut Context,
args: Vec<Value>,
) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-accessible-directory-p", &args)? {
return Ok(result);
}
expect_args("file-accessible-directory-p", &args, 1)?;
let filename = expect_lisp_string_strict(&args[0])?;
let filename = resolve_filename_lisp_for_eval(eval, &filename);
Ok(Value::bool_val(file_accessible_directory_path(
&lisp_file_name_to_path_buf(&filename),
)))
}
pub(crate) fn builtin_file_executable_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-executable-p", &args)? {
return Ok(result);
}
expect_args("file-executable-p", &args, 1)?;
let filename = expect_lisp_string_strict(&args[0])?;
let filename = resolve_filename_lisp_for_eval(eval, &filename);
Ok(Value::bool_val(file_executable_path(
&lisp_file_name_to_path_buf(&filename),
)))
}
pub(crate) fn builtin_file_acl(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-acl", &args)? {
return Ok(result);
}
expect_args("file-acl", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let _filename = resolve_filename_for_eval(eval, &filename);
Ok(Value::NIL)
}
pub(crate) fn builtin_set_file_acl(args: Vec<Value>) -> EvalResult {
expect_args("set-file-acl", &args, 2)?;
let _filename = &args[0];
let _acl = &args[1];
Ok(Value::NIL)
}
pub(crate) fn builtin_file_locked_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-locked-p", &args)? {
return Ok(result);
}
expect_args("file-locked-p", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let filename = resolve_filename_for_eval(eval, &filename);
Ok(Value::bool_val(file_locked_p(&filename)))
}
pub(crate) fn builtin_file_selinux_context(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-selinux-context", &args)? {
return Ok(result);
}
expect_args("file-selinux-context", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let _filename = resolve_filename_for_eval(eval, &filename);
Ok(Value::list(vec![
Value::NIL,
Value::NIL,
Value::NIL,
Value::NIL,
]))
}
pub(crate) fn builtin_set_file_selinux_context(args: Vec<Value>) -> EvalResult {
expect_args("set-file-selinux-context", &args, 2)?;
let _filename = expect_string_strict(&args[0])?;
let _context = &args[1];
Ok(Value::NIL)
}
pub(crate) fn builtin_file_system_info(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-system-info", &args)? {
return Ok(result);
}
expect_args("file-system-info", &args, 1)?;
let filename = expect_lisp_string_strict(&args[0])?;
let filename = resolve_filename_lisp_for_eval(eval, &filename);
let (total, free, avail) = file_system_info_path(&lisp_file_name_to_path_buf(&filename))
.map_err(|err| {
signal_file_action_error_value(
err,
"Getting file system info",
Value::heap_string(filename),
)
})?;
Ok(Value::list(vec![
Value::fixnum(total),
Value::fixnum(free),
Value::fixnum(avail),
]))
}
pub(crate) fn builtin_file_directory_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-directory-p", &args)? {
return Ok(result);
}
expect_args("file-directory-p", &args, 1)?;
let filename = expect_lisp_string_strict(&args[0])?;
let filename = resolve_filename_lisp_for_eval(eval, &filename);
Ok(Value::bool_val(file_directory_path(
&lisp_file_name_to_path_buf(&filename),
)))
}
pub(crate) fn builtin_file_regular_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-regular-p", &args)? {
return Ok(result);
}
expect_args("file-regular-p", &args, 1)?;
let filename = expect_lisp_string_strict(&args[0])?;
let filename = resolve_filename_lisp_for_eval(eval, &filename);
Ok(Value::bool_val(file_regular_path(
&lisp_file_name_to_path_buf(&filename),
)))
}
pub(crate) fn builtin_file_symlink_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-symlink-p", &args)? {
return Ok(result);
}
expect_args("file-symlink-p", &args, 1)?;
let filename = expect_lisp_string_strict(&args[0])?;
let filename = resolve_filename_lisp_for_eval(eval, &filename);
Ok(match file_symlink_target_lisp(&filename) {
Some(target) => Value::heap_string(target),
None => Value::NIL,
})
}
pub(crate) fn builtin_file_name_case_insensitive_p(
eval: &mut Context,
args: Vec<Value>,
) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-name-case-insensitive-p", &args)? {
return Ok(result);
}
expect_args("file-name-case-insensitive-p", &args, 1)?;
let filename = expect_lisp_string_strict(&args[0])?;
let filename = resolve_filename_lisp_for_eval(eval, &filename);
Ok(Value::bool_val(file_name_case_insensitive_path(
&lisp_file_name_to_path_buf(&filename),
)))
}
pub(crate) fn builtin_file_newer_than_file_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
expect_args("file-newer-than-file-p", &args, 2)?;
let file1 = expand_and_dir_to_file_lisp_for_eval(eval, &expect_lisp_string_strict(&args[0])?);
let file2 = expand_and_dir_to_file_lisp_for_eval(eval, &expect_lisp_string_strict(&args[1])?);
if let Some(result) = maybe_dispatch_resolved_file_handler(
eval,
"file-newer-than-file-p",
Some(&file1),
Some(&file2),
vec![
Value::heap_string(file1.clone()),
Value::heap_string(file2.clone()),
],
)? {
return Ok(result);
}
Ok(Value::bool_val(file_newer_than_file_path(
&lisp_file_name_to_path_buf(&file1),
&lisp_file_name_to_path_buf(&file2),
)))
}
pub(crate) fn builtin_file_modes(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "file-modes", &args)? {
return Ok(result);
}
expect_min_args("file-modes", &args, 1)?;
if args.len() > 2 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("file-modes"),
Value::fixnum(args.len() as i64),
],
));
}
let filename = expect_lisp_string_strict(&args[0])?;
let filename = resolve_filename_lisp_for_eval(eval, &filename);
let nofollow = args
.get(1)
.is_some_and(|flag| flag.as_symbol_name() == Some("nofollow"));
match file_modes_path(&lisp_file_name_to_path_buf(&filename), nofollow) {
Some(mode) => Ok(Value::fixnum(mode as i64)),
None => Ok(Value::NIL),
}
}
pub(crate) fn builtin_set_file_modes(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "set-file-modes", &args)? {
return Ok(result);
}
expect_min_args("set-file-modes", &args, 2)?;
if args.len() > 3 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("set-file-modes"),
Value::fixnum(args.len() as i64),
],
));
}
let filename = expect_lisp_string_strict(&args[0])?;
let resolved = resolve_filename_lisp_for_eval(eval, &filename);
let mode = expect_fixnum(&args[1])?;
let nofollow = args
.get(2)
.is_some_and(|flag| flag.as_symbol_name() == Some("nofollow"));
set_file_modes_path(&lisp_file_name_to_path_buf(&resolved), mode, nofollow).map_err(|err| {
signal_file_action_error_value(err, "Doing chmod", Value::heap_string(resolved))
})?;
Ok(Value::NIL)
}
pub(crate) fn builtin_set_file_times(eval: &mut Context, args: Vec<Value>) -> EvalResult {
expect_min_args("set-file-times", &args, 1)?;
if args.len() > 3 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("set-file-times"),
Value::fixnum(args.len() as i64),
],
));
}
let filename = resolve_filename_lisp_for_eval(eval, &expect_lisp_string_strict(&args[0])?);
let mut handler_args = Vec::with_capacity(args.len());
handler_args.push(Value::heap_string(filename.clone()));
handler_args.extend_from_slice(&args[1..]);
if let Some(result) = maybe_dispatch_resolved_file_handler(
eval,
"set-file-times",
Some(&filename),
None,
handler_args,
)? {
return Ok(result);
}
let timestamp = if args.len() > 1 && !args[1].is_nil() {
Some(parse_timestamp_arg(&args[1])?)
} else {
None
};
let nofollow = args.get(2).is_some_and(|flag| !flag.is_nil());
set_file_times_path(&lisp_file_name_to_path_buf(&filename), timestamp, nofollow).map_err(
|err| {
signal_file_action_error_value(err, "Setting file times", Value::heap_string(filename))
},
)?;
Ok(Value::T)
}
fn validate_optional_buffer_arg_in_state(
buffers: &crate::buffer::BufferManager,
arg: Option<&Value>,
) -> Result<(), Flow> {
if let Some(bufferish) = arg {
match bufferish.kind() {
ValueKind::Nil => Ok(()),
ValueKind::Veclike(VecLikeType::Buffer) => {
if let Some(buf_id) = bufferish.as_buffer_id() {
if buffers.get(buf_id).is_some() {
Ok(())
} else {
Err(signal(
"wrong-type-argument",
vec![Value::symbol("bufferp"), *bufferish],
))
}
} else {
Err(signal(
"wrong-type-argument",
vec![Value::symbol("bufferp"), *bufferish],
))
}
}
_ => Err(signal(
"wrong-type-argument",
vec![Value::symbol("bufferp"), *bufferish],
)),
}?
}
Ok(())
}
fn validate_set_visited_file_modtime_arg(arg: &Value) -> Result<(), Flow> {
match arg.kind() {
ValueKind::Fixnum(_) => Ok(()),
ValueKind::String => Err(signal(
"error",
vec![Value::string("Invalid time specification")],
)),
ValueKind::Float | ValueKind::Cons => Ok(()),
_ => Err(signal(
"error",
vec![Value::string("Invalid time specification")],
)),
}
}
pub(crate) fn builtin_visited_file_modtime(args: Vec<Value>) -> EvalResult {
expect_args("visited-file-modtime", &args, 0)?;
Ok(Value::fixnum(0))
}
pub(crate) fn builtin_verify_visited_file_modtime(
eval: &mut Context,
args: Vec<Value>,
) -> EvalResult {
expect_max_args("verify-visited-file-modtime", &args, 1)?;
validate_optional_buffer_arg_in_state(&eval.buffers, args.first())?;
Ok(Value::T)
}
pub(crate) fn builtin_set_visited_file_modtime(eval: &mut Context, args: Vec<Value>) -> EvalResult {
expect_max_args("set-visited-file-modtime", &args, 1)?;
if let Some(arg) = args.first() {
if !arg.is_nil() {
validate_set_visited_file_modtime_arg(arg)?;
return Ok(Value::NIL);
}
}
let file_name = eval
.buffers
.current_buffer()
.and_then(|buf| buf.file_name_value().as_runtime_string_owned());
if file_name.is_none() {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), Value::NIL],
));
}
Ok(Value::NIL)
}
pub(crate) fn builtin_set_default_file_modes(args: Vec<Value>) -> EvalResult {
expect_args("set-default-file-modes", &args, 1)?;
init_default_file_mode_mask();
let mode = expect_fixnum(&args[0])?;
let new_mask = (!mode) & 0o777;
#[cfg(unix)]
unsafe {
libc::umask(new_mask as libc::mode_t);
}
DEFAULT_FILE_MODE_MASK.store(new_mask as u32, Ordering::Relaxed);
Ok(Value::NIL)
}
pub(crate) fn builtin_default_file_modes(args: Vec<Value>) -> EvalResult {
expect_args("default-file-modes", &args, 0)?;
init_default_file_mode_mask();
let mask = DEFAULT_FILE_MODE_MASK.load(Ordering::Relaxed) as i64;
Ok(Value::fixnum((!mask) & 0o777))
}
pub(crate) fn builtin_delete_file(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "delete-file", &args)? {
return Ok(result);
}
expect_min_args("delete-file", &args, 1)?;
if args.len() > 2 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("delete-file"),
Value::fixnum(args.len() as i64),
],
));
}
let filename = expect_lisp_string_strict(&args[0])?;
let resolved = resolve_filename_lisp_for_eval(eval, &filename);
delete_file_compat_path(
&lisp_file_name_to_path_buf(&resolved),
Value::heap_string(resolved),
)?;
Ok(Value::NIL)
}
pub(crate) fn builtin_delete_file_internal(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "delete-file", &args)? {
return Ok(result);
}
expect_args("delete-file-internal", &args, 1)?;
let filename = expect_lisp_string_strict(&args[0])?;
let resolved = resolve_filename_lisp_for_eval(eval, &filename);
delete_file_compat_path(
&lisp_file_name_to_path_buf(&resolved),
Value::heap_string(resolved),
)?;
Ok(Value::NIL)
}
pub(crate) fn builtin_delete_directory_internal(
eval: &mut Context,
args: Vec<Value>,
) -> EvalResult {
expect_args("delete-directory-internal", &args, 1)?;
let directory = expect_lisp_string_strict(&args[0])?;
let resolved = lisp_directory_file_name(&resolve_filename_lisp_for_eval(eval, &directory));
fs::remove_dir(lisp_file_name_to_path_buf(&resolved)).map_err(|err| {
signal_file_action_error_value(err, "Removing directory", Value::heap_string(resolved))
})?;
Ok(Value::NIL)
}
pub(crate) fn builtin_delete_directory(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "delete-directory", &args)? {
return Ok(result);
}
expect_min_args("delete-directory", &args, 1)?;
if args.len() > 3 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("delete-directory"),
Value::fixnum(args.len() as i64),
],
));
}
let directory = expect_lisp_string_strict(&args[0])?;
let directory = lisp_directory_file_name(&resolve_filename_lisp_for_eval(eval, &directory));
let recursive = args.get(1).is_some_and(|value| value.is_truthy());
let result = if recursive {
fs::remove_dir_all(lisp_file_name_to_path_buf(&directory))
} else {
fs::remove_dir(lisp_file_name_to_path_buf(&directory))
};
result.map_err(|err| {
signal_file_action_error_value(err, "Removing directory", Value::heap_string(directory))
})?;
Ok(Value::NIL)
}
pub(crate) fn builtin_make_symbolic_link(eval: &mut Context, args: Vec<Value>) -> EvalResult {
expect_min_args("make-symbolic-link", &args, 2)?;
if args.len() > 3 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("make-symbolic-link"),
Value::fixnum(args.len() as i64),
],
));
}
let mut target = expect_lisp_string_strict(&args[0])?;
if matches!(
args.get(2).map(|value| value.kind()),
Some(ValueKind::Fixnum(_))
) {
if lisp_file_name_is_ascii_text(&target, b"~") || target.as_bytes().starts_with(b"~/") {
target = expand_file_name_lisp(&target, None);
} else if let Some(stripped) = lisp_string_strip_ascii_prefix(&target, b"/:") {
target = stripped;
}
}
let linkname_arg = expect_lisp_string_strict(&args[1])?;
let linkname = expand_cp_target_lisp_for_eval(eval, &target, &linkname_arg);
let mut handler_args = Vec::with_capacity(args.len());
handler_args.push(Value::heap_string(target.clone()));
handler_args.push(Value::heap_string(linkname.clone()));
if let Some(extra) = args.get(2) {
handler_args.push(*extra);
}
if let Some(result) = maybe_dispatch_resolved_file_handler(
eval,
"make-symbolic-link",
None,
Some(&linkname),
handler_args,
)? {
return Ok(result);
}
let ok_if_exists = args.get(2).is_some_and(|value| value.is_truthy());
let link_path = lisp_file_name_to_path_buf(&linkname);
#[cfg(unix)]
{
if fs::symlink_metadata(&link_path).is_ok() {
if !ok_if_exists {
return Err(signal_existing_path_value(
&link_path,
Value::heap_string(linkname.clone()),
));
}
fs::remove_file(&link_path).map_err(|err| {
signal_file_action_error_value(
err,
"Removing old name",
Value::heap_string(linkname.clone()),
)
})?;
}
std::os::unix::fs::symlink(lisp_file_name_to_path_buf(&target), &link_path).map_err(
|err| {
signal_file_action_error_pair_values(
err,
"Making symbolic link",
Value::heap_string(target),
Value::heap_string(linkname),
)
},
)?;
Ok(Value::NIL)
}
#[cfg(not(unix))]
{
let _ = (target, linkname, ok_if_exists);
Err(signal(
"file-error",
vec![Value::string(
"Symbolic links are unsupported on this platform",
)],
))
}
}
pub(crate) fn builtin_rename_file(eval: &mut Context, args: Vec<Value>) -> EvalResult {
expect_min_args("rename-file", &args, 2)?;
if args.len() > 3 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("rename-file"),
Value::fixnum(args.len() as i64),
],
));
}
let from = resolve_filename_lisp_for_eval(eval, &expect_lisp_string_strict(&args[0])?);
let to = expand_cp_target_lisp_for_eval(
eval,
&lisp_directory_file_name(&from),
&expect_lisp_string_strict(&args[1])?,
);
let mut handler_args = Vec::with_capacity(args.len());
handler_args.push(Value::heap_string(from.clone()));
handler_args.push(Value::heap_string(to.clone()));
if let Some(extra) = args.get(2) {
handler_args.push(*extra);
}
if let Some(result) = maybe_dispatch_resolved_file_handler(
eval,
"rename-file",
Some(&from),
Some(&to),
handler_args,
)? {
return Ok(result);
}
let ok_if_exists = args.get(2).is_some_and(|value| value.is_truthy());
let to_path = lisp_file_name_to_path_buf(&to);
if fs::symlink_metadata(&to_path).is_ok() && !ok_if_exists {
return Err(signal_existing_path_value(
&to_path,
Value::heap_string(to.clone()),
));
}
fs::rename(lisp_file_name_to_path_buf(&from), &to_path).map_err(|err| {
signal_file_action_error_pair_values(
err,
"Renaming",
Value::heap_string(from),
Value::heap_string(to),
)
})?;
Ok(Value::NIL)
}
pub(crate) fn builtin_copy_file(eval: &mut Context, args: Vec<Value>) -> EvalResult {
expect_min_args("copy-file", &args, 2)?;
if args.len() > 6 {
return Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol("copy-file"), Value::fixnum(args.len() as i64)],
));
}
let from = resolve_filename_lisp_for_eval(eval, &expect_lisp_string_strict(&args[0])?);
let to = expand_cp_target_lisp_for_eval(eval, &from, &expect_lisp_string_strict(&args[1])?);
let mut handler_args = Vec::with_capacity(args.len());
handler_args.push(Value::heap_string(from.clone()));
handler_args.push(Value::heap_string(to.clone()));
handler_args.extend_from_slice(&args[2..]);
if let Some(result) = maybe_dispatch_resolved_file_handler(
eval,
"copy-file",
Some(&from),
Some(&to),
handler_args,
)? {
return Ok(result);
}
let ok_if_exists = args.get(2).is_some_and(|value| value.is_truthy());
let to_path = lisp_file_name_to_path_buf(&to);
if fs::symlink_metadata(&to_path).is_ok() && !ok_if_exists {
return Err(signal_existing_path_value(
&to_path,
Value::heap_string(to.clone()),
));
}
fs::copy(lisp_file_name_to_path_buf(&from), &to_path).map_err(|err| {
signal_file_action_error_pair_values(
err,
"Copying",
Value::heap_string(from),
Value::heap_string(to),
)
})?;
Ok(Value::NIL)
}
pub(crate) fn builtin_add_name_to_file(eval: &mut Context, args: Vec<Value>) -> EvalResult {
expect_min_args("add-name-to-file", &args, 2)?;
if args.len() > 3 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("add-name-to-file"),
Value::fixnum(args.len() as i64),
],
));
}
let oldname = resolve_filename_lisp_for_eval(eval, &expect_lisp_string_strict(&args[0])?);
let newname =
expand_cp_target_lisp_for_eval(eval, &oldname, &expect_lisp_string_strict(&args[1])?);
let mut handler_args = Vec::with_capacity(args.len());
handler_args.push(Value::heap_string(oldname.clone()));
handler_args.push(Value::heap_string(newname.clone()));
if let Some(extra) = args.get(2) {
handler_args.push(*extra);
}
if let Some(result) = maybe_dispatch_resolved_file_handler(
eval,
"add-name-to-file",
Some(&oldname),
Some(&newname),
handler_args,
)? {
return Ok(result);
}
let ok_if_exists = args.get(2).is_some_and(|value| value.is_truthy());
let newname_path = lisp_file_name_to_path_buf(&newname);
if fs::symlink_metadata(&newname_path).is_ok() {
if !ok_if_exists {
return Err(signal_existing_path_value(
&newname_path,
Value::heap_string(newname.clone()),
));
}
fs::remove_file(&newname_path).map_err(|err| {
signal_file_action_error_value(
err,
"Removing old name",
Value::heap_string(newname.clone()),
)
})?;
}
fs::hard_link(lisp_file_name_to_path_buf(&oldname), &newname_path).map_err(|err| {
signal_file_action_error_pair_values(
err,
"Adding new name",
Value::heap_string(oldname),
Value::heap_string(newname),
)
})?;
Ok(Value::NIL)
}
pub(crate) fn builtin_make_directory_internal(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "make-directory", &args)? {
return Ok(result);
}
expect_args("make-directory-internal", &args, 1)?;
let dir = expect_lisp_string_strict(&args[0])?;
let resolved = resolve_filename_lisp_for_eval(eval, &dir);
fs::create_dir(lisp_file_name_to_path_buf(&resolved)).map_err(|e| {
signal_file_action_error_value(e, "Creating directory", Value::heap_string(resolved))
})?;
Ok(Value::NIL)
}
pub(crate) fn builtin_find_file_name_handler(eval: &mut Context, args: Vec<Value>) -> EvalResult {
expect_args("find-file-name-handler", &args, 2)?;
let filename = match args[0].kind() {
ValueKind::String => args[0]
.as_lisp_string()
.expect("ValueKind::String must carry LispString payload"),
_ => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), args[0]],
));
}
};
let operation = args[1];
Ok(find_file_name_handler_lisp(
&eval.obarray,
filename,
operation,
))
}
pub(crate) fn find_file_name_handler(obarray: &Obarray, filename: &str, operation: Value) -> Value {
let filename = super::builtins::runtime_string_to_lisp_string(filename, !filename.is_ascii());
find_file_name_handler_lisp(obarray, &filename, operation)
}
pub(crate) fn find_file_name_handler_lisp(
obarray: &Obarray,
filename: &crate::heap_types::LispString,
operation: Value,
) -> Value {
let alist = match obarray.symbol_value("file-name-handler-alist") {
Some(v) if v.is_cons() => *v,
_ => return Value::NIL,
};
let mut inhibited: Option<Value> = None;
if let Some(inh_op) = obarray.symbol_value("inhibit-file-name-operation").copied() {
if !inh_op.is_nil() && super::value::eq_value(&inh_op, &operation) {
inhibited = obarray.symbol_value("inhibit-file-name-handlers").copied();
}
}
let mut best: Value = Value::NIL;
let mut best_pos: i64 = -1;
let mut cursor = alist;
while cursor.is_cons() {
let entry = cursor.cons_car();
cursor = cursor.cons_cdr();
if !entry.is_cons() {
continue;
}
let regexp_val = entry.cons_car();
let handler = entry.cons_cdr();
let Some(regexp) = regexp_val.as_lisp_string() else {
continue;
};
if let Some(handler_sym) = handler.as_symbol_id() {
let ops_sym = super::intern::intern("operations");
if let Some(ops) = obarray
.get_property_id(handler_sym, ops_sym)
.filter(|v| !v.is_nil())
{
let mut op_cursor = ops;
let mut found = false;
while op_cursor.is_cons() {
if super::value::eq_value(&op_cursor.cons_car(), &operation) {
found = true;
break;
}
op_cursor = op_cursor.cons_cdr();
}
if !found {
continue;
}
}
}
let mut match_data: Option<crate::emacs_core::regex::MatchData> = None;
let match_pos =
match super::regex::string_match_full_with_case_fold_source_lisp_pattern_posix(
regexp,
filename,
crate::emacs_core::regex::SearchedString::Owned(filename.clone()),
0,
false,
false,
&mut match_data,
) {
Ok(Some(pos)) => pos as i64,
_ => continue,
};
if match_pos > best_pos {
if let Some(inh) = inhibited {
let mut inh_cursor = inh;
let mut skip = false;
while inh_cursor.is_cons() {
if super::value::eq_value(&inh_cursor.cons_car(), &handler) {
skip = true;
break;
}
inh_cursor = inh_cursor.cons_cdr();
}
if skip {
continue;
}
}
best = handler;
best_pos = match_pos;
}
}
best
}
pub(crate) fn dispatch_file_handler(
eval: &mut Context,
operation_name: &str,
args: &[Value],
) -> Result<Option<Value>, super::error::Flow> {
let Some(first) = args.first() else {
return Ok(None);
};
let Some(filename) = first.as_lisp_string() else {
return Ok(None);
};
let operation_sym = Value::symbol(operation_name);
let handler = find_file_name_handler_lisp(&eval.obarray, filename, operation_sym);
if handler.is_nil() {
return Ok(None);
}
let mut call_args = Vec::with_capacity(args.len() + 1);
call_args.push(operation_sym);
call_args.extend_from_slice(args);
let result = eval.funcall_general(handler, call_args)?;
Ok(Some(result))
}
pub(crate) fn dispatch_file_handler_two_arg(
eval: &mut Context,
operation_name: &str,
args: &[Value],
) -> Result<Option<Value>, super::error::Flow> {
if args.len() < 2 {
return Ok(None);
}
let operation_sym = Value::symbol(operation_name);
if let Some(src) = args[0].as_lisp_string() {
let handler = find_file_name_handler_lisp(&eval.obarray, src, operation_sym);
if !handler.is_nil() {
let mut call_args = Vec::with_capacity(args.len() + 1);
call_args.push(operation_sym);
call_args.extend_from_slice(args);
return Ok(Some(eval.funcall_general(handler, call_args)?));
}
}
if let Some(dst) = args[1].as_lisp_string() {
let handler = find_file_name_handler_lisp(&eval.obarray, dst, operation_sym);
if !handler.is_nil() {
let mut call_args = Vec::with_capacity(args.len() + 1);
call_args.push(operation_sym);
call_args.extend_from_slice(args);
return Ok(Some(eval.funcall_general(handler, call_args)?));
}
}
Ok(None)
}
pub(crate) fn builtin_directory_files(eval: &mut Context, args: Vec<Value>) -> EvalResult {
if let Some(result) = dispatch_file_handler(eval, "directory-files", &args)? {
return Ok(result);
}
expect_min_args("directory-files", &args, 1)?;
if args.len() > 5 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("directory-files"),
Value::fixnum(args.len() as i64),
],
));
}
let dir = resolve_filename_for_eval(eval, &expect_string_strict(&args[0])?);
let full = args.get(1).is_some_and(|v| v.is_truthy());
let match_pattern = if let Some(val) = args.get(2) {
if val.is_truthy() {
Some(expect_string_strict(val)?)
} else {
None
}
} else {
None
};
let nosort = args.get(3).is_some_and(|v| v.is_truthy());
let count = if let Some(val) = args.get(4) {
match val.kind() {
ValueKind::Fixnum(n) if n >= 0 => Some(n as usize),
_other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("natnump"), *val],
));
}
}
} else {
None
};
let files = directory_files(&dir, full, match_pattern.as_deref(), nosort, count)
.map_err(|e| signal_directory_files_error(e, &dir))?;
Ok(Value::list(files.into_iter().map(Value::string).collect()))
}
fn expect_int(value: &Value) -> Result<i64, Flow> {
match value.kind() {
ValueKind::Fixnum(n) => Ok(n),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), *value],
)),
}
}
fn expect_file_offset(value: &Value) -> Result<i64, Flow> {
let offset = expect_int(value)?;
if offset < 0 {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("file-offset"), *value],
));
}
Ok(offset)
}
fn current_buffer_id_or_error(
buffers: &crate::buffer::BufferManager,
) -> Result<crate::buffer::BufferId, Flow> {
buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))
}
fn replace_accessible_portion_in_current_buffer(
buffers: &mut crate::buffer::BufferManager,
current_id: crate::buffer::BufferId,
text: &str,
) -> Result<(), Flow> {
let (start, end, old_point) = {
let buf = buffers
.get(current_id)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
(buf.point_min_byte(), buf.point_max_byte(), buf.point_byte())
};
if start < end {
buffers
.delete_buffer_region(current_id, start, end)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
}
buffers
.goto_buffer_byte(current_id, start)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
if !text.is_empty() {
buffers
.insert_into_buffer(current_id, text)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
}
let replacement_end = start + text.len();
let restored_point = if old_point <= start {
old_point
} else {
replacement_end.min(start + (old_point - start))
};
buffers
.goto_buffer_byte(current_id, restored_point)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
Ok(())
}
fn insert_file_contents_into_current_buffer_in_state(
buffers: &mut crate::buffer::BufferManager,
current_id: crate::buffer::BufferId,
contents: &str,
replace_requested: bool,
) -> Result<(), Flow> {
if replace_requested {
replace_accessible_portion_in_current_buffer(buffers, current_id, contents)
} else {
let pt_before = buffers
.get(current_id)
.map(|b| (b.pt_byte, b.pt))
.unwrap_or((0, 0));
buffers
.insert_into_buffer(current_id, contents)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
if let Some(buf) = buffers.get_mut(current_id) {
buf.pt_byte = pt_before.0;
buf.pt = pt_before.1;
}
Ok(())
}
}
fn expect_inserted_char_count(value: &Value) -> Result<i64, Flow> {
let inserted = expect_int(value)?;
if inserted < 0 {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("integer-or-marker-p"), *value],
));
}
Ok(inserted)
}
fn run_after_insert_file_pipeline(
eval: &mut Context,
current_id: crate::buffer::BufferId,
visit: bool,
replace_requested: bool,
inserted_chars: i64,
) -> Result<i64, Flow> {
let visit_value = if visit { Value::T } else { Value::NIL };
let mut inserted = inserted_chars;
if eval.obarray.fboundp("after-insert-file-set-coding") {
let result = eval.funcall_general(
Value::symbol("after-insert-file-set-coding"),
vec![Value::fixnum(inserted), visit_value],
)?;
if !result.is_nil() {
inserted = expect_inserted_char_count(&result)?;
}
}
if inserted <= 0 || !eval.obarray.fboundp("format-decode") {
return Ok(inserted);
}
let (saved_pt, saved_pt_char, point_min, chars_modiff_before) = eval
.buffers
.get(current_id)
.map(|buf| {
(
buf.pt_byte,
buf.pt,
buf.point_min_byte(),
buf.chars_modified_tick(),
)
})
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let specpdl_count = eval.specpdl.len();
eval.specbind(intern("inhibit-point-motion-hooks"), Value::T);
eval.specbind(intern("inhibit-modification-hooks"), Value::T);
eval.specbind(intern("buffer-undo-list"), Value::T);
let pipeline_result = (|| -> Result<i64, Flow> {
if replace_requested {
eval.buffers
.goto_buffer_byte(current_id, point_min)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
}
let format_result = eval.funcall_general(
Value::symbol("format-decode"),
vec![Value::NIL, Value::fixnum(inserted), visit_value],
)?;
if !format_result.is_nil() {
inserted = expect_inserted_char_count(&format_result)?;
}
let hook_sym = intern("after-insert-file-functions");
let hook_value = eval.visible_variable_value_or_nil("after-insert-file-functions");
let hook_functions = crate::emacs_core::hook_runtime::collect_hook_functions_in_state(
eval, hook_sym, hook_value, true,
);
if !hook_functions.is_empty() {
let gc_roots = eval.save_specpdl_roots();
for func in &hook_functions {
eval.push_specpdl_root(*func);
}
eval.push_specpdl_root(Value::fixnum(inserted));
let hook_result = (|| -> Result<i64, Flow> {
let mut inserted_now = inserted;
for function in &hook_functions {
let result = eval.apply(*function, vec![Value::fixnum(inserted_now)])?;
if !result.is_nil() {
inserted_now = expect_inserted_char_count(&result)?;
}
}
Ok(inserted_now)
})();
eval.restore_specpdl_roots(gc_roots);
inserted = hook_result?;
}
Ok(inserted)
})();
eval.restore_current_buffer_if_live(current_id);
let chars_modiff_after = eval
.buffers
.get(current_id)
.map(|buf| buf.chars_modified_tick())
.unwrap_or(chars_modiff_before + 1);
if replace_requested
&& chars_modiff_after == chars_modiff_before
&& let Some(buf) = eval.buffers.get_mut(current_id)
{
buf.pt_byte = saved_pt;
buf.pt = saved_pt_char;
}
eval.unbind_to(specpdl_count);
pipeline_result
}
fn write_region_content_in_state(
buffers: &crate::buffer::BufferManager,
current_id: crate::buffer::BufferId,
start: &Value,
end: Option<&Value>,
) -> Result<crate::heap_types::LispString, Flow> {
if start.is_string() {
return start.as_lisp_string().cloned().ok_or_else(|| {
signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), *start],
)
});
}
let buf = buffers
.get(current_id)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
if start.is_nil() {
return Ok(buf.buffer_substring_lisp_string(buf.point_min(), buf.point_max()));
}
let end = end.unwrap_or(&Value::NIL);
let start = expect_int(start)?;
let end = expect_int(end)?;
let point_min = buf.point_min_char() as i64 + 1;
let point_max = buf.point_max_char() as i64 + 1;
if start < point_min || start > point_max || end < point_min || end > point_max {
return Err(signal(
"args-out-of-range",
vec![
Value::make_buffer(buf.id),
Value::fixnum(start),
Value::fixnum(end),
],
));
}
let (char_start, char_end) = if start <= end {
(start as usize - 1, end as usize - 1)
} else {
(end as usize - 1, start as usize - 1)
};
let byte_start = buf.lisp_pos_to_accessible_byte(char_start as i64 + 1);
let byte_end = buf.lisp_pos_to_accessible_byte(char_end as i64 + 1);
Ok(buf.buffer_substring_lisp_string(byte_start, byte_end))
}
fn decode_insert_file_contents(
coding_systems: &crate::emacs_core::coding::CodingSystemManager,
bytes: &[u8],
multibyte: bool,
source_load_context: bool,
coding_system_for_read: Option<&str>,
) -> Result<(String, String), Flow> {
let detected_default_eol_suffix = |bytes: &[u8]| {
let mut saw_lf = false;
let mut saw_crlf = false;
let mut saw_lone_cr = false;
let mut idx = 0;
while idx < bytes.len() {
match bytes[idx] {
b'\n' => saw_lf = true,
b'\r' => {
if bytes.get(idx + 1) == Some(&b'\n') {
saw_crlf = true;
idx += 1;
} else {
saw_lone_cr = true;
}
}
_ => {}
}
idx += 1;
}
if saw_lf {
"-unix"
} else if saw_crlf {
"-dos"
} else if saw_lone_cr {
"-mac"
} else {
"-unix"
}
};
let is_utf8_like_source_coding = |coding: &str| {
let family = coding
.strip_suffix("-unix")
.or_else(|| coding.strip_suffix("-dos"))
.or_else(|| coding.strip_suffix("-mac"))
.unwrap_or(coding);
matches!(family, "utf-8" | "utf-8-emacs")
};
let Some(coding) =
coding_system_for_read.filter(|coding| !coding.is_empty() && *coding != "nil")
else {
if source_load_context && multibyte {
let eol_suffix = detected_default_eol_suffix(bytes);
return Ok((
crate::encoding::decode_bytes(bytes, &format!("utf-8-emacs{eol_suffix}")),
format!("utf-8-emacs{eol_suffix}"),
));
}
if !multibyte {
return Ok((
crate::emacs_core::string_escape::bytes_to_unibyte_storage_string(bytes),
"no-conversion".to_string(),
));
}
let eol_suffix = detected_default_eol_suffix(bytes);
if bytes.is_ascii() {
let coding = format!("undecided{eol_suffix}");
return Ok((crate::encoding::decode_bytes(bytes, &coding), coding));
}
if std::str::from_utf8(bytes).is_ok() {
let coding = format!("utf-8{eol_suffix}");
return Ok((crate::encoding::decode_bytes(bytes, &coding), coding));
}
let eol_suffix = detected_default_eol_suffix(bytes);
let coding = format!("utf-8-emacs{eol_suffix}");
let decoded = crate::encoding::decode_bytes(bytes, &coding);
return Ok((decoded, coding));
};
let eol_suffix = detected_default_eol_suffix(bytes);
let coding = coding_systems
.canonical_name_for_detected_eol(coding, eol_suffix)
.unwrap_or_else(|| coding.to_string());
if source_load_context && multibyte && is_utf8_like_source_coding(&coding) {
return Ok((crate::emacs_core::load::decode_emacs_utf8(bytes), coding));
}
let decoded = crate::encoding::builtin_decode_coding_string_with_known(
vec![
Value::heap_string(crate::heap_types::LispString::from_unibyte(bytes.to_vec())),
Value::symbol(&coding),
],
|name| coding_systems.is_known_or_derived(name),
)?;
match decoded.kind() {
ValueKind::String => Ok((fileio_owned_runtime_string(decoded), coding)),
other => Err(signal(
"error",
vec![Value::string(format!(
"decode-coding-string returned non-string: {other:?}"
))],
)),
}
}
fn decide_auto_coding_for_insert_file_contents(
eval: &mut super::eval::Context,
filename: crate::heap_types::LispString,
bytes: &[u8],
) -> Result<Option<String>, Flow> {
let function = eval.visible_variable_value_or_nil("set-auto-coding-function");
if function.is_nil() || bytes.is_empty() {
return Ok(None);
}
let saved_current = eval.buffers.current_buffer_id();
let work_buffer = eval
.buffers
.find_buffer_by_name(" *code-converting-work*")
.unwrap_or_else(|| eval.buffers.create_buffer(" *code-converting-work*"));
let restore_and_finish = |eval: &mut super::eval::Context, result: EvalResult| {
if let Some(saved) = saved_current {
eval.restore_current_buffer_if_live(saved);
}
result
};
eval.set_current_buffer_unrecorded(work_buffer)?;
let old_len = eval
.buffers
.get(work_buffer)
.map(|buf| buf.text.len())
.unwrap_or(0);
eval.buffers
.delete_buffer_region(work_buffer, 0, old_len)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
eval.buffers
.set_buffer_multibyte_flag(work_buffer, false)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let raw = crate::heap_types::LispString::from_unibyte(bytes.to_vec());
eval.buffers
.insert_lisp_string_into_buffer(work_buffer, &raw)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
if let Some(buf) = eval.buffers.get_mut(work_buffer) {
buf.goto_byte(0);
}
let result = eval.apply(
function,
vec![
Value::heap_string(filename),
Value::fixnum(bytes.len() as i64),
],
);
let value = restore_and_finish(eval, result)?;
let Some(name) = value
.as_symbol_name()
.map(str::to_owned)
.or_else(|| value.as_runtime_string_owned())
else {
return Ok(None);
};
if eval.coding_systems.is_known_or_derived(&name) {
Ok(Some(name))
} else {
Ok(None)
}
}
pub(crate) fn builtin_insert_file_contents(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
expect_min_args("insert-file-contents", &args, 1)?;
expect_max_args("insert-file-contents", &args, 5)?;
let coding_val = eval.visible_variable_value_or_nil("coding-system-for-read");
let coding_system_for_read: Option<String> = match coding_val.kind() {
ValueKind::Nil => None,
ValueKind::Symbol(id) => Some(resolve_sym(id).to_owned()),
ValueKind::String => Some(fileio_owned_runtime_string(coding_val)),
_ => None,
};
let source_load_context = eval
.visible_variable_value_or_nil("set-auto-coding-for-load")
.is_truthy();
let resolved = resolve_filename_lisp_for_eval(eval, &expect_lisp_string_strict(&args[0])?);
let mut handler_args = Vec::with_capacity(args.len());
handler_args.push(Value::heap_string(resolved.clone()));
handler_args.extend_from_slice(&args[1..]);
if let Some(result) = maybe_dispatch_resolved_file_handler(
eval,
"insert-file-contents",
Some(&resolved),
None,
handler_args,
)? {
return Ok(result);
}
let visit = args.get(1).is_some_and(|v| v.is_truthy());
let replace_requested = args.get(4).is_some_and(|v| !v.is_nil());
let pre_state = eval.buffers.current_buffer().map(|buf| {
if replace_requested {
(
buf.point_min_byte(),
buf.point_max_byte(),
super::editfns::byte_span_char_len(buf, buf.point_min_byte(), buf.point_max_byte()),
)
} else {
(buf.pt_byte, buf.pt_byte, 0)
}
});
let empty_undo_list_p = eval
.buffers
.current_buffer()
.is_some_and(|buf| visit && buf.get_undo_list().is_nil() && buf.text.is_empty());
if let Some((beg, end, _old_len)) = pre_state {
super::editfns::signal_before_change(eval, beg, end)?;
}
let current_id = current_buffer_id_or_error(&eval.buffers)?;
{
let buf = eval
.buffers
.get(current_id)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
if visit
&& (!args.get(2).is_none_or(|v| v.is_nil()) || !args.get(3).is_none_or(|v| v.is_nil()))
{
return Err(signal(
"error",
vec![Value::string("Attempt to visit less than an entire file")],
));
}
if visit && buf.base_buffer.is_some() {
return Err(signal(
"error",
vec![Value::string(
"Cannot do file visiting in an indirect buffer",
)],
));
}
if visit && !replace_requested && !buf.text.is_empty() {
return Err(signal(
"error",
vec![Value::string(
"Cannot do file visiting in a non-empty buffer",
)],
));
}
if crate::emacs_core::editfns::buffer_read_only_active_in_state(&eval.obarray, &[], buf) {
return Err(signal(
"buffer-read-only",
vec![Value::make_buffer(current_id)],
));
}
}
let contents_bytes = match fs::read(lisp_file_name_to_path_buf(&resolved)) {
Ok(contents) => contents,
Err(err) => {
if visit {
let _ = eval
.buffers
.set_buffer_file_name(current_id, Value::heap_string(resolved.clone()));
let _ = eval.buffers.set_buffer_modified_flag(current_id, false);
if empty_undo_list_p {
let _ = eval
.buffers
.configure_buffer_undo_list(current_id, Value::NIL);
}
}
return Err(signal_file_action_error_value(
err,
"Opening input file",
Value::heap_string(resolved.clone()),
));
}
};
let file_len = contents_bytes.len() as i64;
let begin = if args.get(2).is_some_and(|v| !v.is_nil()) {
expect_file_offset(args.get(2).expect("checked above"))?
} else {
0
};
let mut end_off = if args.get(3).is_some_and(|v| !v.is_nil()) {
expect_file_offset(args.get(3).expect("checked above"))?
} else {
file_len
};
if begin > file_len {
return Err(signal(
"file-error",
vec![
Value::string("Read error"),
Value::string("Bad address"),
Value::heap_string(resolved.clone()),
],
));
}
if end_off > file_len {
end_off = file_len;
}
if end_off < begin {
end_off = begin;
}
let slice = &contents_bytes[begin as usize..end_off as usize];
let multibyte = eval
.buffers
.get(current_id)
.map(|buffer| buffer.get_multibyte())
.unwrap_or(true);
let auto_coding_system = if multibyte && coding_system_for_read.is_none() {
decide_auto_coding_for_insert_file_contents(eval, resolved.clone(), slice)?
} else {
None
};
let (contents, used_coding) = decode_insert_file_contents(
&eval.coding_systems,
slice,
multibyte,
source_load_context,
coding_system_for_read
.as_deref()
.or(auto_coding_system.as_deref()),
)?;
let decoded_char_count = contents.chars().count() as i64;
insert_file_contents_into_current_buffer_in_state(
&mut eval.buffers,
current_id,
&contents,
replace_requested,
)?;
eval.set_variable("last-coding-system-used", Value::symbol(&used_coding));
let inserted_char_count = run_after_insert_file_pipeline(
eval,
current_id,
visit,
replace_requested,
decoded_char_count,
)?;
if visit {
let _ = eval
.buffers
.set_buffer_file_name(current_id, Value::heap_string(resolved.clone()));
let _ = eval.buffers.set_buffer_modified_flag(current_id, false);
if empty_undo_list_p {
let _ = eval
.buffers
.configure_buffer_undo_list(current_id, Value::NIL);
}
}
let value = Value::list(vec![
Value::heap_string(resolved),
Value::fixnum(inserted_char_count),
]);
if let Some((beg, _old_end, old_len)) = pre_state {
let new_end = eval
.buffers
.current_buffer()
.map(|buf| {
if replace_requested {
buf.point_max_byte()
} else {
buf.pt_byte
}
})
.unwrap_or(beg);
super::editfns::signal_after_change(eval, beg, new_end, old_len)?;
}
Ok(value)
}
fn resolve_write_coding_system(
eval: &super::eval::Context,
buffers: &crate::buffer::BufferManager,
buffer_id: crate::buffer::BufferId,
) -> String {
let coding_system_for_write = eval.visible_variable_value_or_nil("coding-system-for-write");
if let Some(name) = coding_system_value_to_name(&coding_system_for_write) {
return name;
}
if let Some(buf) = buffers.get(buffer_id) {
if let Some(val) = buf.get_buffer_local("buffer-file-coding-system") {
if let Some(name) = coding_system_value_to_name(&val) {
return name;
}
}
}
"utf-8".to_string()
}
fn coding_system_value_to_name(val: &Value) -> Option<String> {
match val.kind() {
ValueKind::Nil => None,
ValueKind::Symbol(id) => {
let name = resolve_sym(id).to_owned();
if name == "nil" { None } else { Some(name) }
}
ValueKind::String => {
let name = fileio_owned_runtime_string(*val);
if name.is_empty() || name == "nil" {
None
} else {
Some(name)
}
}
_ => None,
}
}
pub(crate) fn builtin_write_region(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
expect_min_args("write-region", &args, 3)?;
expect_max_args("write-region", &args, 7)?;
let resolved = resolve_filename_lisp_for_eval(eval, &expect_lisp_string_strict(&args[2])?);
let visit_file = match args.get(4) {
Some(v) if v.is_t() => Some(resolved.clone()),
Some(v) if v.is_string() => Some(resolve_filename_lisp_for_eval(
eval,
&expect_lisp_string_strict(v)?,
)),
_ => None,
};
let op = Value::symbol("write-region");
let handler = find_file_name_handler_lisp(&eval.obarray, &resolved, op);
if !handler.is_nil() {
let mut call_args = Vec::with_capacity(args.len() + 1);
call_args.push(op);
call_args.extend_from_slice(&args);
call_args[3] = Value::heap_string(resolved.clone());
return eval.funcall_general(handler, call_args);
}
if handler.is_nil() {
if let Some(visit_arg) = args.get(4).and_then(|value| value.as_lisp_string()) {
let visit_handler = find_file_name_handler_lisp(&eval.obarray, visit_arg, op);
if !visit_handler.is_nil() {
let mut call_args = Vec::with_capacity(args.len() + 1);
call_args.push(op);
call_args.extend_from_slice(&args);
call_args[3] = Value::heap_string(resolved.clone());
return eval.funcall_general(visit_handler, call_args);
}
}
}
let resolved_path = lisp_file_name_to_path_buf(&resolved);
let append_mode = match args.get(3) {
Some(value) if value.is_fixnum() || value.is_char() => {
FileWriteMode::Seek(expect_file_offset(value)? as u64)
}
Some(value) if value.is_truthy() => FileWriteMode::Append,
_ => FileWriteMode::Truncate,
};
let current_id = current_buffer_id_or_error(&eval.buffers)?;
if visit_file.is_some() {
let buf = eval
.buffers
.get(current_id)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
if buf.base_buffer.is_some() {
return Err(signal(
"error",
vec![Value::string(
"Cannot do file visiting in an indirect buffer",
)],
));
}
}
let content = write_region_content_in_state(&eval.buffers, current_id, &args[0], args.get(1))?;
let coding_system = resolve_write_coding_system(eval, &eval.buffers, current_id);
let encoded_bytes = crate::encoding::encode_lisp_string(&content, &coding_system);
let file = write_bytes_to_file_with_mode(&encoded_bytes, &resolved_path, append_mode).map_err(
|err| {
signal_file_action_error_value(err, "Writing to", Value::heap_string(resolved.clone()))
},
)?;
let inhibit_fsync = eval
.visible_variable_value_or_nil("write-region-inhibit-fsync")
.is_truthy();
if !inhibit_fsync {
file.sync_all().map_err(|err| {
signal_file_action_error_value(err, "Writing to", Value::heap_string(resolved.clone()))
})?;
}
drop(file);
let wrote_message_path = visit_file.clone();
if let Some(visit_path) = visit_file {
let _ = eval
.buffers
.set_buffer_file_name(current_id, Value::heap_string(visit_path));
let _ = eval.buffers.set_buffer_modified_flag(current_id, false);
}
eval.set_variable("last-coding-system-used", Value::symbol(&coding_system));
if let Some(path) = wrote_message_path.filter(|_| !eval.noninteractive()) {
crate::emacs_core::builtins::builtin_message(
eval,
vec![Value::string("Wrote %s"), Value::heap_string(path)],
)?;
}
Ok(Value::NIL)
}
pub(crate) fn builtin_find_file_noselect(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
expect_min_args("find-file-noselect", &args, 1)?;
expect_max_args("find-file-noselect", &args, 4)?;
let abs_path = resolve_filename_lisp_for_eval(eval, &expect_lisp_string_strict(&args[0])?);
let abs_path_buf = lisp_file_name_to_path_buf(&abs_path);
let rawfile = args.get(2).is_some_and(|value| !value.is_nil());
for buf_id in eval.buffers.buffer_list() {
if let Some(buf) = eval.buffers.get(buf_id) {
if buf
.file_name_value()
.as_lisp_string()
.is_some_and(|name| name == &abs_path)
{
return Ok(Value::make_buffer(buf_id));
}
}
}
let buf_name = crate::emacs_core::builtins::runtime_string_from_lisp_string(
&lisp_file_name_nondirectory(&abs_path),
);
let unique_name = eval.buffers.generate_new_buffer_name(&buf_name);
let buf_id = eval.buffers.create_buffer(&unique_name);
let saved_current = eval.buffers.current_buffer_id();
let open_result = (|| -> EvalResult {
eval.switch_current_buffer(buf_id)?;
let visit_error = if file_exists_path(&abs_path_buf) {
builtin_insert_file_contents(
eval,
vec![Value::heap_string(abs_path.clone()), Value::T],
)?;
let _ = eval.buffers.goto_buffer_byte(buf_id, 0);
Value::NIL
} else {
Value::T
};
let _ = eval
.buffers
.set_buffer_file_name(buf_id, Value::heap_string(abs_path.clone()));
let truename = file_truename_lisp(&abs_path, None)?;
let _ = eval
.buffers
.set_buffer_file_truename(buf_id, Value::heap_string(truename));
if let Some(default_directory) = lisp_file_name_directory(&abs_path) {
let _ = eval.buffers.set_buffer_local_property(
buf_id,
"default-directory",
Value::heap_string(default_directory),
);
}
let _ = eval.buffers.set_buffer_modified_flag(buf_id, false);
if !rawfile && eval.obarray().symbol_function("after-find-file").is_some() {
let warn = Value::bool_val(args.get(1).is_none_or(|value| value.is_nil()));
let _ =
eval.funcall_general(Value::symbol("after-find-file"), vec![visit_error, warn])?;
}
Ok(Value::make_buffer(buf_id))
})();
if let Some(prev_id) = saved_current {
eval.restore_current_buffer_if_live(prev_id);
}
open_result
}
fn make_auto_save_file_name_for_buffer(
obarray: &Obarray,
buf: &crate::buffer::Buffer,
) -> crate::heap_types::LispString {
if let Some(file_name) = buf.file_name_lisp_string() {
let dir = lisp_file_name_directory(file_name)
.unwrap_or_else(|| file_name_lisp_from_bytes(Vec::new(), file_name.is_multibyte()));
let base =
wrap_ascii_around_lisp_string(&lisp_file_name_nondirectory(file_name), b"#", b"#");
concat_file_name_lisp(&dir, &base)
} else {
let dir = obarray
.symbol_value("auto-save-list-file-prefix")
.and_then(|value| value.as_lisp_string().cloned())
.and_then(|value| {
if value.as_bytes().is_empty() {
None
} else {
lisp_file_name_directory(&value)
}
})
.or_else(|| {
obarray
.symbol_value("temporary-file-directory")
.and_then(|value| value.as_lisp_string().cloned())
})
.unwrap_or_else(|| crate::heap_types::LispString::from_utf8("/tmp/"));
let name = buf
.name_value()
.as_lisp_string()
.expect("buffer name must be a Lisp string");
let mut safe_name_bytes = name.as_bytes().to_vec();
for byte in &mut safe_name_bytes {
if *byte == b'/' {
*byte = b'!';
}
}
let safe_name = file_name_lisp_from_bytes(safe_name_bytes, name.is_multibyte());
let base = wrap_ascii_around_lisp_string(&safe_name, b"#*", b"*#");
concat_file_name_lisp(&lisp_file_name_as_directory(&dir), &base)
}
}
pub(crate) fn builtin_make_auto_save_file_name(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
expect_min_args("make-auto-save-file-name", &args, 0)?;
expect_max_args("make-auto-save-file-name", &args, 0)?;
let current_id = current_buffer_id_or_error(&eval.buffers)?;
let auto_name = {
let buf = eval
.buffers
.get(current_id)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
if let Some(file_name) = buf.file_name_lisp_string() {
let op = Value::symbol("make-auto-save-file-name");
let handler = find_file_name_handler_lisp(&eval.obarray, file_name, op);
if !handler.is_nil() {
let result = eval.funcall_general(handler, vec![op])?;
if result.is_string() {
if let Some(buf) = eval.buffers.get_mut(current_id) {
buf.set_buffer_local("buffer-auto-save-file-name", result);
buf.set_auto_save_file_name_value(result);
}
}
return Ok(result);
}
}
make_auto_save_file_name_for_buffer(&eval.obarray, buf)
};
let auto_name_value = Value::heap_string(auto_name.clone());
if let Some(buf) = eval.buffers.get_mut(current_id) {
buf.set_buffer_local("buffer-auto-save-file-name", auto_name_value);
buf.set_auto_save_file_name_value(auto_name_value);
}
Ok(auto_name_value)
}
pub(crate) fn builtin_do_auto_save(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
expect_min_args("do-auto-save", &args, 0)?;
expect_max_args("do-auto-save", &args, 2)?;
let _no_message = args.first().is_some_and(|v| v.is_truthy());
let current_only = args.get(1).is_some_and(|v| v.is_truthy());
let auto_save_visited = eval
.obarray
.symbol_value("auto-save-visited-file-name")
.is_some_and(|v| v.is_truthy());
let buffer_ids: Vec<crate::buffer::BufferId> = if current_only {
eval.buffers.current_buffer_id().into_iter().collect()
} else {
eval.buffers.buffer_list()
};
for buf_id in buffer_ids {
let (auto_save_name, file_name, content_bytes, content_len) = {
let Some(buf) = eval.buffers.get(buf_id) else {
continue;
};
if buf.name_starts_with_space() {
continue;
}
if buf.base_buffer.is_some() {
continue;
}
if let Some(saved_size_val) = buf.get_buffer_local("buffer-saved-size") {
if let Some(n) = saved_size_val.as_fixnum() {
if n < 0 {
continue;
}
}
}
if buf.autosave_modified_tick >= buf.modified_tick() {
continue;
}
if !buf.is_modified() {
continue;
}
let auto_name = buf.auto_save_file_name_lisp_string().cloned();
let visit_name = buf.file_name_lisp_string().cloned();
let mut bytes = Vec::new();
buf.copy_emacs_bytes_to(0, buf.total_bytes(), &mut bytes);
(auto_name, visit_name, bytes, buf.total_bytes() as i64)
};
let target = if auto_save_visited {
file_name.clone()
} else {
auto_save_name.clone()
};
let Some(target_path) = target else {
let auto_name = {
let buf = eval.buffers.get(buf_id).unwrap();
make_auto_save_file_name_for_buffer(&eval.obarray, buf)
};
let auto_name_value = Value::heap_string(auto_name.clone());
if let Some(buf) = eval.buffers.get_mut(buf_id) {
buf.set_buffer_local("buffer-auto-save-file-name", auto_name_value);
buf.set_auto_save_file_name_value(auto_name_value);
}
let _ = write_bytes_to_file_with_mode(
&content_bytes,
&lisp_file_name_to_path_buf(&auto_name),
FileWriteMode::Truncate,
);
let _ = eval.buffers.set_buffer_auto_saved(buf_id);
if let Some(buf) = eval.buffers.get_mut(buf_id) {
buf.set_buffer_local("buffer-saved-size", Value::fixnum(content_len));
}
continue;
};
if write_bytes_to_file_with_mode(
&content_bytes,
&lisp_file_name_to_path_buf(&target_path),
FileWriteMode::Truncate,
)
.is_ok()
{
let _ = eval.buffers.set_buffer_auto_saved(buf_id);
if let Some(buf) = eval.buffers.get_mut(buf_id) {
buf.set_buffer_local("buffer-saved-size", Value::fixnum(content_len));
}
}
}
Ok(Value::NIL)
}
pub fn register_bootstrap_vars(obarray: &mut crate::emacs_core::symbol::Obarray) {
let temporary_file_directory = std::env::temp_dir().to_string_lossy().to_string();
obarray.set_symbol_value("file-name-coding-system", Value::NIL);
obarray.set_symbol_value("default-file-name-coding-system", Value::NIL);
obarray.set_symbol_value("set-auto-coding-for-load", Value::NIL);
obarray.set_symbol_value("file-name-handler-alist", Value::NIL);
obarray.set_symbol_value("set-auto-coding-function", Value::NIL);
obarray.set_symbol_value("after-insert-file-functions", Value::NIL);
obarray.set_symbol_value("write-region-annotate-functions", Value::NIL);
obarray.set_symbol_value("write-region-post-annotation-function", Value::NIL);
obarray.set_symbol_value("write-region-annotations-so-far", Value::NIL);
obarray.set_symbol_value("inhibit-file-name-handlers", Value::NIL);
obarray.set_symbol_value("inhibit-file-name-operation", Value::NIL);
obarray.set_symbol_value("directory-abbrev-alist", Value::NIL);
obarray.set_symbol_value("auto-save-list-file-name", Value::NIL);
obarray.set_symbol_value("auto-save-list-file-prefix", Value::NIL);
obarray.set_symbol_value("auto-save-visited-file-name", Value::NIL);
obarray.set_symbol_value("auto-save-include-big-deletions", Value::NIL);
obarray.set_symbol_value("small-temporary-file-directory", Value::NIL);
obarray.set_symbol_value("write-region-inhibit-fsync", Value::NIL);
obarray.set_symbol_value("delete-by-moving-to-trash", Value::NIL);
obarray.set_symbol_value("auto-save-file-name-transforms", Value::NIL);
obarray.set_symbol_value(
"temporary-file-directory",
Value::string(temporary_file_directory),
);
obarray.set_symbol_value("create-lockfiles", Value::T);
obarray.set_symbol_value("make-backup-files", Value::T);
obarray.set_symbol_value("backup-inhibited", Value::NIL);
obarray.set_symbol_value("version-control", Value::NIL);
obarray.set_symbol_value("backup-directory-alist", Value::NIL);
obarray.set_symbol_value(
"locate-dominating-stop-dir-regexp",
Value::string(r"\`\(?:[\\/][\\/][^\\/]+[\\/]\|/\(?:net\|afs\|\.\.\.\)/\)\'"),
);
}
#[cfg(test)]
#[path = "fileio_test.rs"]
mod tests;