use std::ffi::OsStr;
use std::io;
use std::path::{Path, PathBuf};
use crate::atom::{Atom, AtomTable};
use crate::io::{IoOp, IoResult, StatxData, errno_to_atom};
use crate::native::{
BifRegistryImpl, Capability, FileIoCompletion, FileIoContinuation, NativeRegistrationError,
ProcessContext,
};
use crate::term::Term;
use crate::term::binary_ref::BinaryRef;
pub fn register_file_meta_bifs(
registry: &BifRegistryImpl,
atom_table: &AtomTable,
) -> Result<(), NativeRegistrationError> {
let erlang = atom_table.intern("erlang");
for (name, arity, function) in [
("file_info", 1, file_info as crate::native::NativeFn),
("list_dir", 1, list_dir as crate::native::NativeFn),
("make_dir", 1, make_dir as crate::native::NativeFn),
("del_file", 1, del_file as crate::native::NativeFn),
("del_dir", 1, del_dir as crate::native::NativeFn),
("rename", 2, rename as crate::native::NativeFn),
] {
registry.register(
erlang,
atom_table.intern(name),
arity,
function,
Capability::ExternalIo,
)?;
}
Ok(())
}
pub fn file_info(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
if let Some(completion) = context.take_file_io_completion() {
return finish_file_info(completion, context);
}
let [filename] = args else {
return Err(badarg());
};
let path = filename_path(*filename)?;
context.submit_file_io(
IoOp::Statx {
dir_fd: libc::AT_FDCWD,
path,
flags: libc::AT_SYMLINK_NOFOLLOW,
mask: statx_basic_stats_mask(),
},
FileIoContinuation::FileInfo,
)?;
Ok(Term::atom(Atom::OK))
}
pub fn list_dir(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
if let Some(completion) = context.take_file_io_completion() {
return finish_list_dir(completion, context);
}
let [dirname] = args else {
return Err(badarg());
};
context.submit_file_io(
IoOp::ListDir {
path: filename_path(*dirname)?,
},
FileIoContinuation::ListDir,
)?;
Ok(Term::atom(Atom::OK))
}
pub fn make_dir(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
submit_unary_metadata(args, context, FileIoContinuation::MakeDir, |path| {
IoOp::MakeDir { path }
})
}
pub fn del_file(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
submit_unary_metadata(args, context, FileIoContinuation::DelFile, |path| {
IoOp::DelFile { path }
})
}
pub fn del_dir(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
submit_unary_metadata(args, context, FileIoContinuation::DelDir, |path| {
IoOp::DelDir { path }
})
}
pub fn rename(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
if let Some(completion) = context.take_file_io_completion() {
return finish_ok_metadata(completion, context);
}
let [source, destination] = args else {
return Err(badarg());
};
context.submit_file_io(
IoOp::Rename {
source: filename_path(*source)?,
destination: filename_path(*destination)?,
},
FileIoContinuation::Rename,
)?;
Ok(Term::atom(Atom::OK))
}
fn submit_unary_metadata<F>(
args: &[Term],
context: &mut ProcessContext,
continuation: FileIoContinuation,
op: F,
) -> Result<Term, Term>
where
F: FnOnce(PathBuf) -> IoOp,
{
if let Some(completion) = context.take_file_io_completion() {
return finish_ok_metadata(completion, context);
}
let [filename] = args else {
return Err(badarg());
};
context.submit_file_io(op(filename_path(*filename)?), continuation)?;
Ok(Term::atom(Atom::OK))
}
fn finish_file_info(
completion: FileIoCompletion,
context: &mut ProcessContext,
) -> Result<Term, Term> {
if !matches!(completion.continuation, FileIoContinuation::FileInfo) {
return error_tuple(context, Atom::UNKNOWN_ERROR);
}
match completion.completion.result {
Ok(IoResult::StatResult(data)) => file_info_tuple(context, &data),
Ok(_) => error_tuple(context, Atom::UNKNOWN_ERROR),
Err(error) => error_tuple(context, error_reason(error)),
}
}
fn finish_list_dir(
completion: FileIoCompletion,
context: &mut ProcessContext,
) -> Result<Term, Term> {
if !matches!(completion.continuation, FileIoContinuation::ListDir) {
return error_tuple(context, Atom::UNKNOWN_ERROR);
}
match completion.completion.result {
Ok(IoResult::DirList(entries)) => {
let mut terms = Vec::with_capacity(entries.len());
for entry in entries {
terms.push(context.alloc_binary(&entry)?);
}
let list = context.alloc_list(&terms)?;
ok_tuple(context, list)
}
Ok(_) => error_tuple(context, Atom::UNKNOWN_ERROR),
Err(error) => error_tuple(context, error_reason(error)),
}
}
fn finish_ok_metadata(
completion: FileIoCompletion,
context: &mut ProcessContext,
) -> Result<Term, Term> {
if !is_ok_metadata_continuation(&completion.continuation) {
return error_tuple(context, Atom::UNKNOWN_ERROR);
}
match completion.completion.result {
Ok(IoResult::Completed) => Ok(Term::atom(Atom::OK)),
Ok(_) => error_tuple(context, Atom::UNKNOWN_ERROR),
Err(error) => error_tuple(context, error_reason(error)),
}
}
fn is_ok_metadata_continuation(continuation: &FileIoContinuation) -> bool {
matches!(
continuation,
FileIoContinuation::MakeDir
| FileIoContinuation::DelFile
| FileIoContinuation::DelDir
| FileIoContinuation::Rename
)
}
fn statx_basic_stats_mask() -> u32 {
#[cfg(target_os = "linux")]
{
libc::STATX_TYPE
| libc::STATX_MODE
| libc::STATX_NLINK
| libc::STATX_UID
| libc::STATX_GID
| libc::STATX_ATIME
| libc::STATX_MTIME
| libc::STATX_CTIME
| libc::STATX_INO
| libc::STATX_SIZE
| libc::STATX_BLOCKS
}
#[cfg(not(target_os = "linux"))]
{
0
}
}
fn file_info_tuple(context: &mut ProcessContext, data: &StatxData) -> Result<Term, Term> {
let atom_table = context.atom_table().ok_or_else(badarg)?;
let fields = [
Term::atom(atom_table.intern("file_info")),
unsigned_term(data.size)?,
Term::atom(file_type_atom(atom_table, data.mode)),
Term::atom(access_atom(atom_table, data.mode)),
signed_term(data.atime_sec)?,
signed_term(data.mtime_sec)?,
signed_term(data.ctime_sec)?,
unsigned_term(u64::from(data.mode))?,
unsigned_term(data.nlink)?,
unsigned_term(u64::from(data.dev_major))?,
unsigned_term(u64::from(data.dev_minor))?,
unsigned_term(data.inode)?,
unsigned_term(u64::from(data.uid))?,
unsigned_term(u64::from(data.gid))?,
];
context.alloc_tuple(&fields)
}
fn file_type_atom(atom_table: &AtomTable, mode: u32) -> Atom {
match mode & libc::S_IFMT as u32 {
value if value == libc::S_IFREG as u32 => atom_table.intern("regular"),
value if value == libc::S_IFDIR as u32 => atom_table.intern("directory"),
value if value == libc::S_IFLNK as u32 => atom_table.intern("symlink"),
value if value == libc::S_IFBLK as u32 || value == libc::S_IFCHR as u32 => {
atom_table.intern("device")
}
_ => atom_table.intern("other"),
}
}
fn access_atom(atom_table: &AtomTable, mode: u32) -> Atom {
let read_bits = (libc::S_IRUSR | libc::S_IRGRP | libc::S_IROTH) as u32;
let write_bits = (libc::S_IWUSR | libc::S_IWGRP | libc::S_IWOTH) as u32;
let readable = mode & read_bits != 0;
let writable = mode & write_bits != 0;
match (readable, writable) {
(true, true) => atom_table.intern("read_write"),
(true, false) => Atom::READ,
(false, true) => Atom::WRITE,
(false, false) => atom_table.intern("none"),
}
}
fn unsigned_term(value: u64) -> Result<Term, Term> {
i64::try_from(value)
.ok()
.and_then(Term::try_small_int)
.ok_or_else(badarg)
}
fn signed_term(value: i64) -> Result<Term, Term> {
Term::try_small_int(value).ok_or_else(badarg)
}
fn filename_path(term: Term) -> Result<PathBuf, Term> {
let bytes = BinaryRef::new(term).ok_or_else(badarg)?.as_bytes();
let filename = std::str::from_utf8(bytes).map_err(|_| badarg())?;
Ok(PathBuf::from(filename))
}
pub(crate) fn os_filename_bytes(name: &OsStr) -> Vec<u8> {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
name.as_bytes().to_vec()
}
#[cfg(not(unix))]
{
name.to_string_lossy().as_bytes().to_vec()
}
}
pub(crate) fn read_dir_entries(path: &Path) -> io::Result<Vec<Vec<u8>>> {
let mut entries = Vec::new();
for entry in std::fs::read_dir(path)? {
let entry = entry?;
entries.push(os_filename_bytes(&entry.file_name()));
}
Ok(entries)
}
fn ok_tuple(context: &mut ProcessContext, value: Term) -> Result<Term, Term> {
context.alloc_tuple(&[Term::atom(Atom::OK), value])
}
fn error_tuple(context: &mut ProcessContext, reason: Atom) -> Result<Term, Term> {
context.alloc_tuple(&[Term::atom(Atom::ERROR), Term::atom(reason)])
}
fn error_reason(error: io::Error) -> Atom {
error
.raw_os_error()
.map(errno_to_atom)
.unwrap_or(Atom::UNKNOWN_ERROR)
}
fn badarg() -> Term {
Term::atom(Atom::BADARG)
}
#[cfg(test)]
#[path = "file_meta_bifs_tests.rs"]
mod tests;