use std::collections::VecDeque;
#[cfg(unix)]
use std::ffi::{CStr, CString};
use std::fs;
use std::io::{ErrorKind, Seek, SeekFrom, Write};
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
}
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 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;
}
}
}
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 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: &str) -> Result<(i64, i64, i64), Flow> {
#[cfg(unix)]
{
fn saturating_i64(v: u128) -> i64 {
if v > i64::MAX as u128 {
i64::MAX
} else {
v as i64
}
}
let c_path = CString::new(path.as_bytes()).map_err(|_| {
signal(
"file-error",
vec![
Value::string("Getting file system info"),
Value::string("embedded NUL in file name"),
Value::string(path),
],
)
})?;
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(signal_file_io_path(
std::io::Error::last_os_error(),
"Getting file system info",
path,
));
}
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_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
}
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(), filename, mode)?;
drop(file);
Ok(())
}
enum FileWriteMode {
Truncate,
Append,
Seek(u64),
}
fn write_bytes_to_file_with_mode(
content: &[u8],
filename: &str,
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 expect_string(value: &Value) -> Result<String, Flow> {
match value.kind() {
ValueKind::String => Ok(value.as_str().unwrap().to_owned()),
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(value.as_str().unwrap().to_owned()),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), *value],
)),
}
}
fn expect_temp_prefix(value: &Value) -> Result<String, Flow> {
match value.kind() {
ValueKind::String => Ok(value.as_str().unwrap().to_owned()),
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_fixnum() || first.is_float() || 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")?;
val.as_str().map(|s| s.to_owned())
}
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_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
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 = expect_string_strict(&args[0])?;
let default_dir = if let Some(arg) = args.get(1) {
match arg.kind() {
ValueKind::Nil => default_directory_in_state(obarray, dynamic, buffers),
ValueKind::String => Some(arg.as_str().unwrap().to_owned()),
_ => Some("/".to_string()),
}
} else {
default_directory_in_state(obarray, dynamic, buffers)
};
Ok(Value::string(expand_file_name(
&name,
default_dir.as_deref(),
)))
}
pub(crate) fn builtin_expand_file_name(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_expand_file_name_impl(&eval.obarray, &[], &eval.buffers, args)
}
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(Value::string(file_name_as_directory(&filename)))
}
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(v.as_str().unwrap().to_owned()),
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_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
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_string_strict(&args[0])?;
if let Some(counter) = args.get(1) {
validate_file_truename_counter(counter)?;
}
Ok(Value::string(file_truename(
&filename,
default_directory_in_state(obarray, dynamic, buffers).as_deref(),
)))
}
pub(crate) fn builtin_file_truename(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_file_truename_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_file_name_directory(args: Vec<Value>) -> EvalResult {
expect_args("file-name-directory", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
match file_name_directory(&filename) {
Some(dir) => Ok(Value::string(dir)),
None => Ok(Value::NIL),
}
}
pub(crate) fn builtin_file_name_nondirectory(args: Vec<Value>) -> EvalResult {
expect_args("file-name-nondirectory", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
Ok(Value::string(file_name_nondirectory(&filename)))
}
pub(crate) fn builtin_file_name_as_directory(args: Vec<Value>) -> EvalResult {
expect_args("file-name-as-directory", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
Ok(Value::string(file_name_as_directory(&filename)))
}
pub(crate) fn builtin_directory_file_name(args: Vec<Value>) -> EvalResult {
expect_args("directory-file-name", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
Ok(Value::string(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 = value.as_str().unwrap().to_owned();
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(args: Vec<Value>) -> EvalResult {
expect_args("substitute-in-file-name", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
Ok(Value::string(substitute_in_file_name(&filename)))
}
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 let Some(s) = val.as_str() {
return Some(s.to_owned());
}
}
}
match obarray.symbol_value("default-directory") {
Some(val) if val.is_string() => Some(val.as_str().unwrap().to_owned()),
_ => None,
}
}
fn default_directory_for_eval(eval: &Context) -> Option<String> {
default_directory_in_state(&eval.obarray, &[], &eval.buffers)
}
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 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 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)),
}
}
pub(crate) fn builtin_access_file_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("access-file", &args, 2)?;
let filename =
resolve_filename_in_state(obarray, dynamic, buffers, &expect_string_strict(&args[0])?);
let operation = expect_string_strict(&args[1])?;
match fs::metadata(&filename) {
Ok(_) => Ok(Value::NIL),
Err(err) => Err(signal_file_action_error(err, &operation, &filename)),
}
}
pub(crate) fn builtin_access_file(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_access_file_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_file_exists_p_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("file-exists-p", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
Ok(Value::bool_val(file_exists_p(&filename)))
}
pub(crate) fn builtin_file_exists_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_file_exists_p_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_file_readable_p_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("file-readable-p", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
Ok(Value::bool_val(file_readable_p(&filename)))
}
pub(crate) fn builtin_file_readable_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_file_readable_p_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_file_writable_p_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("file-writable-p", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
Ok(Value::bool_val(file_writable_p(&filename)))
}
pub(crate) fn builtin_file_writable_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_file_writable_p_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_file_accessible_directory_p_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("file-accessible-directory-p", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
Ok(Value::bool_val(file_accessible_directory_p(&filename)))
}
pub(crate) fn builtin_file_accessible_directory_p(
eval: &mut Context,
args: Vec<Value>,
) -> EvalResult {
builtin_file_accessible_directory_p_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_file_executable_p_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("file-executable-p", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
Ok(Value::bool_val(file_executable_p(&filename)))
}
pub(crate) fn builtin_file_executable_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_file_executable_p_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_file_acl_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("file-acl", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let _filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
Ok(Value::NIL)
}
pub(crate) fn builtin_file_acl(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_file_acl_impl(&eval.obarray, &[], &eval.buffers, args)
}
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_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("file-locked-p", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
Ok(Value::bool_val(file_locked_p(&filename)))
}
pub(crate) fn builtin_file_locked_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_file_locked_p_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_file_selinux_context_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("file-selinux-context", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let _filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
Ok(Value::list(vec![
Value::NIL,
Value::NIL,
Value::NIL,
Value::NIL,
]))
}
pub(crate) fn builtin_file_selinux_context(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_file_selinux_context_impl(&eval.obarray, &[], &eval.buffers, args)
}
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_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("file-system-info", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
let (total, free, avail) = file_system_info(&filename)?;
Ok(Value::list(vec![
Value::fixnum(total),
Value::fixnum(free),
Value::fixnum(avail),
]))
}
pub(crate) fn builtin_file_system_info(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_file_system_info_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_file_directory_p_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("file-directory-p", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
Ok(Value::bool_val(file_directory_p(&filename)))
}
pub(crate) fn builtin_file_directory_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_file_directory_p_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_file_regular_p_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("file-regular-p", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
Ok(Value::bool_val(file_regular_p(&filename)))
}
pub(crate) fn builtin_file_regular_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_file_regular_p_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_file_symlink_p_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("file-symlink-p", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
Ok(Value::bool_val(file_symlink_p(&filename)))
}
pub(crate) fn builtin_file_symlink_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_file_symlink_p_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_file_name_case_insensitive_p_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("file-name-case-insensitive-p", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
Ok(Value::bool_val(file_name_case_insensitive_p(&filename)))
}
pub(crate) fn builtin_file_name_case_insensitive_p(
eval: &mut Context,
args: Vec<Value>,
) -> EvalResult {
builtin_file_name_case_insensitive_p_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_file_newer_than_file_p_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("file-newer-than-file-p", &args, 2)?;
let file1 = expect_string_strict(&args[0])?;
let file2 = expect_string_strict(&args[1])?;
let file1 = resolve_filename_in_state(obarray, dynamic, buffers, &file1);
let file2 = resolve_filename_in_state(obarray, dynamic, buffers, &file2);
Ok(Value::bool_val(file_newer_than_file_p(&file1, &file2)))
}
pub(crate) fn builtin_file_newer_than_file_p(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_file_newer_than_file_p_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_file_modes_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
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_string_strict(&args[0])?;
let filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
match file_modes(&filename) {
Some(mode) => Ok(Value::fixnum(mode as i64)),
None => Ok(Value::NIL),
}
}
pub(crate) fn builtin_file_modes(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_file_modes_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_set_file_modes_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
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_string_strict(&args[0])?;
let filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
let mode = expect_fixnum(&args[1])?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = fs::Permissions::from_mode(mode as u32);
fs::set_permissions(&filename, perms)
.map_err(|err| signal_file_action_error(err, "Doing chmod", &filename))?;
}
#[cfg(not(unix))]
{
let mut perms = fs::metadata(&filename)
.map_err(|err| signal_file_action_error(err, "Doing chmod", &filename))?
.permissions();
let writable = (mode & 0o222) != 0;
perms.set_readonly(!writable);
fs::set_permissions(&filename, perms)
.map_err(|err| signal_file_action_error(err, "Doing chmod", &filename))?;
}
Ok(Value::NIL)
}
pub(crate) fn builtin_set_file_modes(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_set_file_modes_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_set_file_times_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
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 = expect_string_strict(&args[0])?;
let filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
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_compat(&filename, timestamp, nofollow)?;
Ok(Value::T)
}
pub(crate) fn builtin_set_file_times(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_set_file_times_impl(&eval.obarray, &[], &eval.buffers, args)
}
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(_) => Err(signal(
"args-out-of-range",
vec![*arg, Value::fixnum(-1), Value::fixnum(0)],
)),
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 {
builtin_verify_visited_file_modtime_impl(&eval.buffers, args)
}
pub(crate) fn builtin_set_visited_file_modtime(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_set_visited_file_modtime_impl(&eval.buffers, args)
}
pub(crate) fn builtin_verify_visited_file_modtime_impl(
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_max_args("verify-visited-file-modtime", &args, 1)?;
validate_optional_buffer_arg_in_state(buffers, args.first())?;
Ok(Value::T)
}
pub(crate) fn builtin_set_visited_file_modtime_impl(
buffers: &crate::buffer::BufferManager,
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 = buffers
.current_buffer()
.and_then(|buf| buf.file_name.clone());
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 {
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_string_strict(&args[0])?;
let filename = resolve_filename_for_eval(eval, &filename);
delete_file_compat(&filename)?;
Ok(Value::NIL)
}
pub(crate) fn builtin_delete_file_internal_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("delete-file-internal", &args, 1)?;
let filename = expect_string_strict(&args[0])?;
let filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
delete_file_compat(&filename)?;
Ok(Value::NIL)
}
pub(crate) fn builtin_delete_file_internal(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_delete_file_internal_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_delete_directory_internal_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("delete-directory-internal", &args, 1)?;
let directory = expect_string_strict(&args[0])?;
let directory = resolve_filename_in_state(obarray, dynamic, buffers, &directory);
fs::remove_dir(&directory)
.map_err(|err| signal_file_io_path(err, "Removing directory", &directory))?;
Ok(Value::NIL)
}
pub(crate) fn builtin_delete_directory_internal(
eval: &mut Context,
args: Vec<Value>,
) -> EvalResult {
builtin_delete_directory_internal_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_delete_directory(eval: &mut Context, args: Vec<Value>) -> EvalResult {
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_string_strict(&args[0])?;
let directory = resolve_filename_for_eval(eval, &directory);
let recursive = args.get(1).is_some_and(|value| value.is_truthy());
let result = if recursive {
fs::remove_dir_all(&directory)
} else {
fs::remove_dir(&directory)
};
result.map_err(|err| signal_file_io_path(err, "Removing directory", &directory))?;
Ok(Value::NIL)
}
pub(crate) fn builtin_make_symbolic_link_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
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 target =
resolve_filename_in_state(obarray, dynamic, buffers, &expect_string_strict(&args[0])?);
let linkname =
resolve_filename_in_state(obarray, dynamic, buffers, &expect_string_strict(&args[1])?);
let ok_if_exists = args.get(2).is_some_and(|value| value.is_truthy());
#[cfg(unix)]
{
if ok_if_exists && fs::symlink_metadata(&linkname).is_ok() {
fs::remove_file(&linkname)
.map_err(|err| signal_file_io_path(err, "Removing old name", &linkname))?;
}
std::os::unix::fs::symlink(&target, &linkname)
.map_err(|err| signal_file_io_path(err, "Making symbolic link", &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_make_symbolic_link(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_make_symbolic_link_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_rename_file_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
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_in_state(obarray, dynamic, buffers, &expect_string_strict(&args[0])?);
let to = resolve_filename_in_state(obarray, dynamic, buffers, &expect_string_strict(&args[1])?);
let ok_if_exists = args.get(2).is_some_and(|value| value.is_truthy());
if fs::symlink_metadata(&to).is_ok() {
if ok_if_exists {
fs::remove_file(&to).map_err(|e| signal_file_io_path(e, "Removing old name", &to))?;
} else {
return Err(signal(
"file-already-exists",
vec![
Value::string("Renaming"),
Value::string(format!("File exists: {to}")),
Value::string(&from),
Value::string(&to),
],
));
}
}
rename_file(&from, &to).map_err(|e| signal_file_io_paths(e, "Renaming", &from, &to))?;
Ok(Value::NIL)
}
pub(crate) fn builtin_rename_file(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_rename_file_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_copy_file_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
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_in_state(obarray, dynamic, buffers, &expect_string_strict(&args[0])?);
let to = resolve_filename_in_state(obarray, dynamic, buffers, &expect_string_strict(&args[1])?);
let ok_if_exists = args.get(2).is_some_and(|value| value.is_truthy());
if fs::symlink_metadata(&to).is_ok() && !ok_if_exists {
return Err(signal(
"file-already-exists",
vec![
Value::string("Copying"),
Value::string(format!("File exists: {to}")),
Value::string(&from),
Value::string(&to),
],
));
}
copy_file(&from, &to).map_err(|e| signal_file_io_paths(e, "Copying", &from, &to))?;
Ok(Value::NIL)
}
pub(crate) fn builtin_copy_file(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_copy_file_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_add_name_to_file_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
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_in_state(obarray, dynamic, buffers, &expect_string_strict(&args[0])?);
let newname =
resolve_filename_in_state(obarray, dynamic, buffers, &expect_string_strict(&args[1])?);
let ok_if_exists = args.get(2).is_some_and(|value| value.is_truthy());
if ok_if_exists && fs::symlink_metadata(&newname).is_ok() {
fs::remove_file(&newname)
.map_err(|err| signal_file_io_path(err, "Removing old name", &newname))?;
}
add_name_to_file(&oldname, &newname)
.map_err(|err| signal_file_io_paths(err, "Adding new name", &oldname, &newname))?;
Ok(Value::NIL)
}
pub(crate) fn builtin_add_name_to_file(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_add_name_to_file_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_make_directory_internal_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("make-directory-internal", &args, 1)?;
let dir = expect_string_strict(&args[0])?;
let dir = resolve_filename_in_state(obarray, dynamic, buffers, &dir);
make_directory(&dir, false).map_err(|e| signal_file_io_path(e, "Creating directory", &dir))?;
Ok(Value::NIL)
}
pub(crate) fn builtin_make_directory_internal(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_make_directory_internal_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_find_file_name_handler_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
expect_args("find-file-name-handler", &args, 2)?;
let filename = expect_string_strict(&args[0])?;
let _filename = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
let _operation = &args[1];
Ok(Value::NIL)
}
pub(crate) fn builtin_find_file_name_handler(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_find_file_name_handler_impl(&eval.obarray, &[], &eval.buffers, args)
}
pub(crate) fn builtin_directory_files_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &crate::buffer::BufferManager,
args: Vec<Value>,
) -> EvalResult {
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_in_state(obarray, dynamic, buffers, &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()))
}
pub(crate) fn builtin_directory_files(eval: &mut Context, args: Vec<Value>) -> EvalResult {
builtin_directory_files_impl(&eval.obarray, &[], &eval.buffers, args)
}
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, b.pt_char))
.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 = pt_before.0;
buf.pt_char = pt_before.1;
}
Ok(())
}
}
fn write_region_content_in_state(
buffers: &crate::buffer::BufferManager,
current_id: crate::buffer::BufferId,
start: &Value,
end: Option<&Value>,
) -> Result<String, Flow> {
if start.is_string() {
return expect_string_strict(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_string());
}
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.text.char_to_byte(char_start.min(buf.text.char_count()));
let byte_end = buf.text.char_to_byte(char_end.min(buf.text.char_count()));
Ok(buf.buffer_substring(byte_start, byte_end))
}
fn decode_insert_file_contents(
bytes: &[u8],
multibyte: bool,
source_load_context: bool,
coding_system_for_read: Option<&str>,
) -> Result<(String, String), Flow> {
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 {
return Ok((
crate::emacs_core::load::decode_emacs_utf8(bytes),
"utf-8-emacs".to_string(),
));
}
if !multibyte {
return Ok((
crate::emacs_core::string_escape::bytes_to_unibyte_storage_string(bytes),
"no-conversion".to_string(),
));
}
let decoded = crate::encoding::decode_bytes(bytes, "utf-8-emacs");
return Ok((decoded, "utf-8-emacs".to_string()));
};
if source_load_context && multibyte && is_utf8_like_source_coding(coding) {
return Ok((
crate::emacs_core::load::decode_emacs_utf8(bytes),
coding.to_string(),
));
}
let decoded = crate::encoding::builtin_decode_coding_string(vec![
Value::unibyte_string(
crate::emacs_core::string_escape::bytes_to_unibyte_storage_string(bytes),
),
Value::symbol(coding),
])?;
match decoded.kind() {
ValueKind::String => Ok((decoded.as_str().unwrap().to_owned(), coding.to_string())),
other => Err(signal(
"error",
vec![Value::string(format!(
"decode-coding-string returned non-string: {other:?}"
))],
)),
}
}
pub(crate) fn builtin_insert_file_contents_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &mut crate::buffer::BufferManager,
source_load_context: bool,
coding_system_for_read: Option<&str>,
args: Vec<Value>,
) -> Result<(Value, String), Flow> {
expect_min_args("insert-file-contents", &args, 1)?;
expect_max_args("insert-file-contents", &args, 5)?;
let filename = expect_string_strict(&args[0])?;
let resolved = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
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 current_id = current_buffer_id_or_error(buffers)?;
{
let buf = 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(obarray, dynamic, buf) {
return Err(signal(
"buffer-read-only",
vec![Value::make_buffer(current_id)],
));
}
}
let contents_bytes =
fs::read(&resolved).map_err(|e| signal_file_io_path(e, "Opening input file", &resolved))?;
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 = 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::string(resolved),
],
));
}
if end > file_len {
end = file_len;
}
if end < begin {
end = begin;
}
let slice = &contents_bytes[begin as usize..end as usize];
let multibyte = buffers
.get(current_id)
.map(|buffer| buffer.multibyte)
.unwrap_or(true);
let (contents, used_coding) = decode_insert_file_contents(
slice,
multibyte,
source_load_context,
coding_system_for_read,
)?;
let char_count = contents.chars().count() as i64;
insert_file_contents_into_current_buffer_in_state(
buffers,
current_id,
&contents,
replace_requested,
)?;
if visit {
let _ = buffers.set_buffer_file_name(current_id, Some(resolved.clone()));
let _ = buffers.set_buffer_modified_flag(current_id, false);
}
Ok((
Value::list(vec![Value::string(resolved), Value::fixnum(char_count)]),
used_coding,
))
}
pub(crate) fn builtin_insert_file_contents(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
let coding_val = eval.visible_variable_value_or_nil("coding-system-for-read");
let coding_system_for_read = match coding_val.kind() {
ValueKind::Nil => None,
ValueKind::Symbol(id) => Some(resolve_sym(id).to_owned()),
ValueKind::String => Some(coding_val.as_str().unwrap().to_owned()),
_ => None,
};
let source_load_context = eval
.visible_variable_value_or_nil("set-auto-coding-for-load")
.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, buf.pt, 0)
}
});
if let Some((beg, end, _old_len)) = pre_state {
super::editfns::signal_before_change(eval, beg, end)?;
}
let (value, used_coding) = builtin_insert_file_contents_impl(
&eval.obarray,
&[],
&mut eval.buffers,
source_load_context,
coding_system_for_read.as_deref(),
args,
)?;
eval.set_variable("last-coding-system-used", Value::symbol(&used_coding));
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
}
})
.unwrap_or(beg);
super::editfns::signal_after_change(eval, beg, new_end, old_len)?;
}
Ok(value)
}
fn next_backup_version_number(filename: &str) -> u32 {
let path = Path::new(filename);
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let base_name = path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
let prefix = format!("{base_name}.~");
let mut max_ver: u32 = 0;
if let Ok(entries) = fs::read_dir(parent) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
if let Some(rest) = name.strip_prefix(&prefix) {
if let Some(num_str) = rest.strip_suffix('~') {
if let Ok(n) = num_str.parse::<u32>() {
max_ver = max_ver.max(n);
}
}
}
}
}
max_ver + 1
}
fn compute_backup_file_name(obarray: &Obarray, filename: &str) -> String {
let backup_dir = lookup_backup_directory(obarray, filename);
let use_numbered = match obarray.symbol_value("version-control") {
Some(v) if v.is_symbol_named("never") => false,
Some(v) if v.is_nil() => {
next_backup_version_number(filename) > 1
}
Some(v) if v.is_truthy() => true,
_ => false,
};
if use_numbered {
let ver = next_backup_version_number(filename);
match backup_dir {
Some(dir) => {
let base = file_name_nondirectory(filename);
format!("{dir}/{base}.~{ver}~")
}
None => format!("{filename}.~{ver}~"),
}
} else {
match backup_dir {
Some(dir) => {
let base = file_name_nondirectory(filename);
format!("{dir}/{base}~")
}
None => format!("{filename}~"),
}
}
}
fn lookup_backup_directory(obarray: &Obarray, filename: &str) -> Option<String> {
let alist_val = obarray.symbol_value("backup-directory-alist")?;
let entries = list_to_vec(alist_val)?;
for entry in &entries {
if entry.is_cons() {
let car = entry.cons_car();
let cdr = entry.cons_cdr();
let pattern = match car.kind() {
ValueKind::String => car.as_str().unwrap().to_owned(),
_ => continue,
};
let dir = match cdr.kind() {
ValueKind::String => cdr.as_str().unwrap().to_owned(),
_ => continue,
};
if pattern == "." || filename.contains(&pattern) {
let dir_path = expand_file_name(&dir, None);
let _ = fs::create_dir_all(&dir_path);
return Some(dir_path);
}
}
}
None
}
fn backup_file_before_save(
obarray: &Obarray,
buffers: &mut crate::buffer::BufferManager,
buffer_id: crate::buffer::BufferId,
filename: &str,
) {
if let Some(v) = obarray.symbol_value("make-backup-files") {
if v.is_nil() {
return;
}
}
if let Some(v) = obarray.symbol_value("backup-inhibited") {
if v.is_truthy() {
return;
}
}
if let Some(buf) = buffers.get(buffer_id) {
if let Some(v) = buf.get_buffer_local("buffer-backed-up") {
if v.is_truthy() {
return;
}
}
}
if !Path::new(filename).exists() {
return;
}
let backup_name = compute_backup_file_name(obarray, filename);
if fs::copy(filename, &backup_name).is_ok() {
if let Some(buf) = buffers.get_mut(buffer_id) {
buf.set_buffer_local("buffer-backed-up", Value::T);
}
}
}
pub(crate) fn builtin_write_region_impl(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buffers: &mut crate::buffer::BufferManager,
args: Vec<Value>,
) -> Result<(Value, String), Flow> {
expect_min_args("write-region", &args, 3)?;
expect_max_args("write-region", &args, 7)?;
let filename = expect_string_strict(&args[2])?;
let resolved = resolve_filename_in_state(obarray, dynamic, buffers, &filename);
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 visit_path = match args.get(4) {
Some(v) if v.is_t() => Some(resolved.clone()),
Some(v) if v.is_string() => Some(resolve_filename_in_state(
obarray,
dynamic,
buffers,
&expect_string_strict(v)?,
)),
_ => None,
};
let current_id = current_buffer_id_or_error(buffers)?;
if visit_path.is_some() {
let buf = 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",
)],
));
}
}
if matches!(append_mode, FileWriteMode::Truncate) {
backup_file_before_save(obarray, buffers, current_id, &resolved);
}
let content = write_region_content_in_state(buffers, current_id, &args[0], args.get(1))?;
let coding_system = resolve_write_coding_system(obarray, buffers, current_id);
let encoded_bytes = crate::encoding::encode_string(&content, &coding_system);
let file = write_bytes_to_file_with_mode(&encoded_bytes, &resolved, append_mode)
.map_err(|e| signal_file_io_path(e, "Writing to", &resolved))?;
let inhibit_fsync = obarray
.symbol_value("write-region-inhibit-fsync")
.is_some_and(|v| v.is_truthy());
if !inhibit_fsync {
file.sync_all()
.map_err(|e| signal_file_io_path(e, "Writing to", &resolved))?;
}
drop(file);
if let Some(visit_path) = visit_path {
let _ = buffers.set_buffer_file_name(current_id, Some(visit_path));
let _ = buffers.set_buffer_modified_flag(current_id, false);
}
Ok((Value::NIL, coding_system))
}
fn resolve_write_coding_system(
obarray: &Obarray,
buffers: &crate::buffer::BufferManager,
buffer_id: crate::buffer::BufferId,
) -> String {
if let Some(val) = obarray.symbol_value("coding-system-for-write") {
if let Some(name) = coding_system_value_to_name(val) {
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 = val.as_str().unwrap().to_owned();
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 {
let (value, used_coding) =
builtin_write_region_impl(&eval.obarray, &[], &mut eval.buffers, args)?;
eval.set_variable("last-coding-system-used", Value::symbol(&used_coding));
Ok(value)
}
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 filename = expect_string(&args[0])?;
let abs_path = resolve_filename_for_eval(eval, &filename);
for buf_id in eval.buffers.buffer_list() {
if let Some(buf) = eval.buffers.get(buf_id) {
if buf.file_name.as_deref() == Some(&abs_path) {
return Ok(Value::make_buffer(buf_id));
}
}
}
let buf_name = 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);
if file_exists_p(&abs_path) {
let contents = read_file_contents(&abs_path)
.map_err(|e| signal_file_io_path(e, "Opening input file", &abs_path))?;
let saved_current = eval.buffers.current_buffer_id();
eval.switch_current_buffer(buf_id)?;
let content_len = contents.len();
super::editfns::signal_before_change(eval, 0, 0)?;
let _ = eval.buffers.insert_into_buffer(buf_id, &contents);
super::editfns::signal_after_change(eval, 0, content_len, 0)?;
let _ = eval.buffers.goto_buffer_byte(buf_id, 0);
let _ = eval
.buffers
.set_buffer_file_name(buf_id, Some(abs_path.clone()));
let _ = eval.buffers.set_buffer_modified_flag(buf_id, false);
if let Some(prev_id) = saved_current {
eval.restore_current_buffer_if_live(prev_id);
}
} else {
let _ = eval.buffers.set_buffer_file_name(buf_id, Some(abs_path));
}
Ok(Value::make_buffer(buf_id))
}
fn make_auto_save_file_name_for_buffer(obarray: &Obarray, buf: &crate::buffer::Buffer) -> String {
if let Some(ref file_name) = buf.file_name {
let dir = file_name_directory(file_name).unwrap_or_default();
let base = file_name_nondirectory(file_name);
format!("{dir}#{base}#")
} else {
let dir = obarray
.symbol_value("auto-save-list-file-prefix")
.and_then(|v| v.as_str_owned())
.and_then(|s| {
if s.is_empty() {
None
} else {
file_name_directory(&s)
}
})
.or_else(|| {
obarray
.symbol_value("temporary-file-directory")
.and_then(|v| v.as_str_owned())
})
.unwrap_or_else(|| "/tmp/".to_string());
let safe_name = buf.name.replace('/', "!");
format!("{dir}#*{safe_name}*#")
}
}
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")]))?;
make_auto_save_file_name_for_buffer(&eval.obarray, buf)
};
if let Some(buf) = eval.buffers.get_mut(current_id) {
buf.set_buffer_local("buffer-auto-save-file-name", Value::string(&auto_name));
buf.auto_save_file_name = Some(auto_name.clone());
}
Ok(Value::string(auto_name))
}
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 (should_save, auto_save_name, file_name, content) = {
let Some(buf) = eval.buffers.get(buf_id) else {
continue;
};
if buf.name.starts_with(' ') {
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.clone();
let visit_name = buf.file_name.clone();
let text = buf.text.text_range(0, buf.text.len());
(true, auto_name, visit_name, text)
};
if !should_save {
continue;
}
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)
};
if let Some(buf) = eval.buffers.get_mut(buf_id) {
buf.set_buffer_local("buffer-auto-save-file-name", Value::string(&auto_name));
buf.auto_save_file_name = Some(auto_name.clone());
}
let _ = write_string_to_file(&content, &auto_name, false);
let _ = eval.buffers.set_buffer_auto_saved(buf_id);
let size = content.len() as i64;
if let Some(buf) = eval.buffers.get_mut(buf_id) {
buf.set_buffer_local("buffer-saved-size", Value::fixnum(size));
}
continue;
};
if write_string_to_file(&content, &target_path, false).is_ok() {
let _ = eval.buffers.set_buffer_auto_saved(buf_id);
let size = content.len() as i64;
if let Some(buf) = eval.buffers.get_mut(buf_id) {
buf.set_buffer_local("buffer-saved-size", Value::fixnum(size));
}
}
}
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;