Current section
Files
Jump to
Current section
Files
native/ex_netfs_nif/src/lib.rs
use rustler::{Atom, Encoder, Env, Term};
mod atoms {
rustler::atoms! {
ok,
error,
// Open option atoms
guest,
allow_loopback,
no_ui,
// Mount option atoms
no_browse,
read_only,
allow_sub_mounts,
soft_mount,
mount_at_dir,
// Error reason atoms
not_found,
permission_denied,
already_mounted,
invalid_argument,
timed_out,
host_unreachable,
host_down,
connection_refused,
user_cancelled,
pwd_needs_change,
pwd_policy,
account_restricted,
no_shares_available,
no_auth_mech,
no_proto_version,
internal_error,
mount_failed,
guest_not_supported,
already_closed,
unknown_error,
// Platform
not_supported,
}
}
#[cfg(target_os = "macos")]
fn error_code_to_atom(code: i32) -> Atom {
match code {
2 => atoms::not_found(),
13 => atoms::permission_denied(),
16 => atoms::already_mounted(),
22 => atoms::invalid_argument(),
60 => atoms::timed_out(),
61 => atoms::connection_refused(),
64 => atoms::host_down(),
65 => atoms::host_unreachable(),
-128 => atoms::user_cancelled(),
-5045 => atoms::pwd_needs_change(),
-5046 => atoms::pwd_policy(),
-5999 => atoms::account_restricted(),
-5998 => atoms::no_shares_available(),
-5997 => atoms::no_auth_mech(),
-5996 => atoms::no_proto_version(),
-6600 => atoms::internal_error(),
-6602 => atoms::mount_failed(),
-6004 => atoms::guest_not_supported(),
-6005 => atoms::already_closed(),
_ => atoms::unknown_error(),
}
}
// ==================== macOS implementation ====================
#[cfg(target_os = "macos")]
mod platform {
use super::*;
use core_foundation::base::{kCFAllocatorDefault, Boolean, CFType, TCFType};
use core_foundation::boolean::CFBoolean;
use core_foundation::dictionary::CFMutableDictionary;
use core_foundation::number::CFNumber;
use core_foundation::string::CFString;
use core_foundation::url::CFURL;
use core_foundation_sys::array::CFArrayRef;
use core_foundation_sys::dictionary::CFMutableDictionaryRef;
use core_foundation_sys::string::CFStringRef;
use core_foundation_sys::url::CFURLRef;
use std::os::raw::{c_char, c_int};
use std::ptr;
// NetFS.framework extern C API
extern "C" {
fn NetFSMountURLSync(
url: CFURLRef,
mountpath: CFURLRef,
user: CFStringRef,
passwd: CFStringRef,
open_options: CFMutableDictionaryRef,
mount_options: CFMutableDictionaryRef,
mountpoints: *mut CFArrayRef,
) -> c_int;
}
// mount.h constants
const MNT_RDONLY: c_int = 0x00000001;
const MNT_FORCE: c_int = 0x00080000;
const MNT_DONTBROWSE: c_int = 0x00100000;
const MNT_NOWAIT: c_int = 2;
// macOS statfs structure (from sys/mount.h)
#[repr(C)]
pub struct Statfs {
pub f_bsize: u32,
pub f_iosize: i32,
pub f_blocks: u64,
pub f_bfree: u64,
pub f_bavail: u64,
pub f_files: u64,
pub f_ffree: u64,
pub f_fsid: [i32; 2],
pub f_owner: u32,
pub f_type: u32,
pub f_flags: u32,
pub f_fssubtype: u32,
pub f_fstypename: [c_char; 16],
pub f_mntonname: [c_char; 1024],
pub f_mntfromname: [c_char; 1024],
pub f_flags_ext: u32,
pub f_reserved: [u32; 7],
}
extern "C" {
fn unmount(dir: *const c_char, flags: c_int) -> c_int;
fn statfs(path: *const c_char, buf: *mut Statfs) -> c_int;
fn getmntinfo(mntbufp: *mut *mut Statfs, flags: c_int) -> c_int;
}
fn build_open_options(opts: &[Atom]) -> CFMutableDictionary<CFString, CFType> {
let mut dict = CFMutableDictionary::<CFString, CFType>::new();
for opt in opts {
if *opt == atoms::guest() {
dict.set(CFString::new("Guest"), CFBoolean::true_value().as_CFType());
} else if *opt == atoms::allow_loopback() {
dict.set(CFString::new("AllowLoopback"), CFBoolean::true_value().as_CFType());
} else if *opt == atoms::no_ui() {
dict.set(CFString::new("UIOption"), CFString::new("NoUI").as_CFType());
}
}
dict
}
fn build_mount_options(opts: &[Atom]) -> CFMutableDictionary<CFString, CFType> {
let mut dict = CFMutableDictionary::<CFString, CFType>::new();
let mut flags: i32 = 0;
for opt in opts {
if *opt == atoms::no_browse() {
flags |= MNT_DONTBROWSE;
} else if *opt == atoms::read_only() {
flags |= MNT_RDONLY;
} else if *opt == atoms::allow_sub_mounts() {
dict.set(CFString::new("AllowSubMounts"), CFBoolean::true_value().as_CFType());
} else if *opt == atoms::soft_mount() {
dict.set(CFString::new("SoftMount"), CFBoolean::true_value().as_CFType());
} else if *opt == atoms::mount_at_dir() {
dict.set(CFString::new("MountAtMountDir"), CFBoolean::true_value().as_CFType());
}
}
if flags != 0 {
let num = CFNumber::from(flags);
dict.set(CFString::new("MountFlags"), num.as_CFType());
}
dict
}
pub fn do_mount(
url_str: &str,
mountpath_str: Option<&str>,
user_str: Option<&str>,
passwd_str: Option<&str>,
open_opts: &[Atom],
mount_opts: &[Atom],
) -> Result<Vec<String>, (Atom, i32)> {
// Build CFURL from string URL (e.g. "smb://server/share")
// core-foundation 0.10 doesn't have from_string, use sys directly
let cf_url_string = CFString::new(url_str);
let url_ref = unsafe {
core_foundation_sys::url::CFURLCreateWithString(
kCFAllocatorDefault,
cf_url_string.as_concrete_TypeRef(),
ptr::null(), // no base URL
)
};
if url_ref.is_null() {
return Err((atoms::invalid_argument(), 22));
}
let url = unsafe { CFURL::wrap_under_create_rule(url_ref) };
let mountpath = mountpath_str.map(|p| {
let cf_path = CFString::new(p);
unsafe {
let url_ref = core_foundation_sys::url::CFURLCreateWithFileSystemPath(
kCFAllocatorDefault,
cf_path.as_concrete_TypeRef(),
core_foundation_sys::url::kCFURLPOSIXPathStyle,
true as Boolean,
);
CFURL::wrap_under_create_rule(url_ref)
}
});
let user = user_str.map(CFString::new);
let passwd = passwd_str.map(CFString::new);
let open_options = build_open_options(open_opts);
let mount_options = build_mount_options(mount_opts);
let mut mountpoints: CFArrayRef = ptr::null();
let result = unsafe {
NetFSMountURLSync(
url.as_concrete_TypeRef(),
mountpath.as_ref().map(|u| u.as_concrete_TypeRef()).unwrap_or(ptr::null()),
user.as_ref().map(|s| s.as_concrete_TypeRef()).unwrap_or(ptr::null()),
passwd.as_ref().map(|s| s.as_concrete_TypeRef()).unwrap_or(ptr::null()),
open_options.as_concrete_TypeRef(),
mount_options.as_concrete_TypeRef(),
&mut mountpoints,
)
};
if result != 0 {
return Err((error_code_to_atom(result), result));
}
let mut paths = Vec::new();
if !mountpoints.is_null() {
unsafe {
let count = core_foundation_sys::array::CFArrayGetCount(mountpoints);
for i in 0..count {
let val = core_foundation_sys::array::CFArrayGetValueAtIndex(mountpoints, i);
if !val.is_null() {
let cf_str = CFString::wrap_under_get_rule(val as CFStringRef);
paths.push(cf_str.to_string());
}
}
core_foundation_sys::base::CFRelease(mountpoints as *const _);
}
}
Ok(paths)
}
pub fn do_unmount(path: &str, force: bool) -> Result<(), (Atom, i32)> {
let c_path = std::ffi::CString::new(path)
.map_err(|_| (atoms::invalid_argument(), 22))?;
let flags = if force { MNT_FORCE } else { 0 };
let result = unsafe { unmount(c_path.as_ptr(), flags) };
if result != 0 {
let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(-1);
Err((error_code_to_atom(errno), errno))
} else {
Ok(())
}
}
pub fn check_mounted(path: &str) -> bool {
let c_path = match std::ffi::CString::new(path) {
Ok(p) => p,
Err(_) => return false,
};
unsafe {
let mut stat: Statfs = std::mem::zeroed();
if statfs(c_path.as_ptr(), &mut stat) == 0 {
let mount_on = std::ffi::CStr::from_ptr(stat.f_mntonname.as_ptr())
.to_string_lossy();
mount_on == path
} else {
false
}
}
}
pub fn get_network_mounts() -> Vec<(String, String, String)> {
let mut mounts = Vec::new();
unsafe {
let mut mntbuf: *mut Statfs = ptr::null_mut();
let count = getmntinfo(&mut mntbuf, MNT_NOWAIT);
if count > 0 && !mntbuf.is_null() {
let entries = std::slice::from_raw_parts(mntbuf, count as usize);
for entry in entries {
let fstype = std::ffi::CStr::from_ptr(entry.f_fstypename.as_ptr())
.to_string_lossy()
.to_string();
if fstype == "smbfs" || fstype == "nfs" || fstype == "afpfs" || fstype == "webdav" {
let mount_from = std::ffi::CStr::from_ptr(entry.f_mntfromname.as_ptr())
.to_string_lossy()
.to_string();
let mount_on = std::ffi::CStr::from_ptr(entry.f_mntonname.as_ptr())
.to_string_lossy()
.to_string();
mounts.push((mount_from, mount_on, fstype));
}
}
}
}
mounts
}
}
// ==================== NIF Exports ====================
/// Mount a network file share via NetFSMountURLSync.
/// Kerberos-aware: if user/password are nil, uses TGT from credential cache.
#[rustler::nif(schedule = "DirtyCpu")]
fn mount<'a>(
env: Env<'a>,
url: String,
mountpath: Option<String>,
user: Option<String>,
password: Option<String>,
open_opts: Vec<Atom>,
mount_opts: Vec<Atom>,
) -> Term<'a> {
#[cfg(target_os = "macos")]
{
match platform::do_mount(
&url,
mountpath.as_deref(),
user.as_deref(),
password.as_deref(),
&open_opts,
&mount_opts,
) {
Ok(paths) => (atoms::ok(), paths).encode(env),
Err((reason, code)) => (atoms::error(), (reason, code)).encode(env),
}
}
#[cfg(not(target_os = "macos"))]
{
let _ = (&url, &mountpath, &user, &password, &open_opts, &mount_opts);
(atoms::error(), (atoms::not_supported(), 0i32)).encode(env)
}
}
/// Unmount a filesystem at the given mount point path.
#[rustler::nif(schedule = "DirtyCpu")]
fn unmount_volume<'a>(env: Env<'a>, path: String, force: bool) -> Term<'a> {
#[cfg(target_os = "macos")]
{
match platform::do_unmount(&path, force) {
Ok(()) => atoms::ok().encode(env),
Err((reason, code)) => (atoms::error(), (reason, code)).encode(env),
}
}
#[cfg(not(target_os = "macos"))]
{
let _ = (&path, force);
(atoms::error(), (atoms::not_supported(), 0i32)).encode(env)
}
}
/// Check if a path is currently a mount point.
#[rustler::nif]
fn is_mounted(path: String) -> bool {
#[cfg(target_os = "macos")]
{
platform::check_mounted(&path)
}
#[cfg(not(target_os = "macos"))]
{
let _ = &path;
false
}
}
/// List all currently mounted network filesystems.
/// Returns [{remote, mountpoint, fstype}] for smbfs, nfs, afpfs, webdav.
#[rustler::nif]
fn list_network_mounts<'a>(env: Env<'a>) -> Term<'a> {
#[cfg(target_os = "macos")]
{
platform::get_network_mounts().encode(env)
}
#[cfg(not(target_os = "macos"))]
{
let empty: Vec<(String, String, String)> = vec![];
empty.encode(env)
}
}
rustler::init!("Elixir.ExNetfs.Native");