use std::{
cell::RefCell,
collections::HashMap,
ffi::OsStr,
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
};
use rustix::fs::{stat, statfs};
use super::SmallBytes;
struct CacheEntry {
mount_point: SmallBytes,
device: SmallBytes,
}
thread_local! {
static CACHE: RefCell<HashMap<u64, CacheEntry>> = RefCell::new(HashMap::new());
}
#[derive(Clone, PartialEq, Eq)]
pub(super) struct Inner {
mount: super::MountPoint,
canonical: PathBuf,
relative_offset: usize,
}
impl Inner {
#[cfg_attr(not(tarpaulin), inline(always))]
pub(super) fn mount_info(&self) -> &super::MountPoint {
&self.mount
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub(super) fn canonical_path(&self) -> &Path {
&self.canonical
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub(super) fn relative_path(&self) -> &Path {
let bytes = self.canonical.as_os_str().as_bytes();
Path::new(OsStr::from_bytes(&bytes[self.relative_offset..]))
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub(super) fn resolve(path: &Path) -> std::io::Result<Inner> {
let canonical = path.canonicalize()?;
let st = stat(&canonical).map_err(std::io::Error::from)?;
let dev = st.st_dev as u64;
let cached = CACHE.with(|c| {
c.borrow()
.get(&dev)
.map(|e| (e.mount_point.clone(), e.device.clone()))
});
#[cfg(not(feature = "disk-usage"))]
let (mount_point, device) = if let Some(hit) = cached {
hit
} else {
let fs = statfs(&canonical).map_err(std::io::Error::from)?;
let mp = SmallBytes::from_bytes(c_chars_as_bytes(&fs.f_mntonname));
let dv = SmallBytes::from_bytes(c_chars_as_bytes(&fs.f_mntfromname));
CACHE.with(|c| {
c.borrow_mut().insert(
dev,
CacheEntry {
mount_point: mp.clone(),
device: dv.clone(),
},
);
});
(mp, dv)
};
#[cfg(feature = "disk-usage")]
#[allow(clippy::unnecessary_cast)]
let (mount_point, device, total_bytes, available_bytes) = if let Some((mp, dv)) = cached {
match statfs(&canonical) {
Ok(fs) => {
let bsize = fs.f_bsize as u64;
(
mp,
dv,
(fs.f_blocks as u64).saturating_mul(bsize),
(fs.f_bavail as u64).saturating_mul(bsize),
)
}
Err(_) => (mp, dv, 0, 0),
}
} else {
let fs = statfs(&canonical).map_err(std::io::Error::from)?;
let mp = SmallBytes::from_bytes(c_chars_as_bytes(&fs.f_mntonname));
let dv = SmallBytes::from_bytes(c_chars_as_bytes(&fs.f_mntfromname));
let bsize = fs.f_bsize as u64;
let total = (fs.f_blocks as u64).saturating_mul(bsize);
let avail = (fs.f_bavail as u64).saturating_mul(bsize);
CACHE.with(|c| {
c.borrow_mut().insert(
dev,
CacheEntry {
mount_point: mp.clone(),
device: dv.clone(),
},
);
});
(mp, dv, total, avail)
};
let canonical_bytes = canonical.as_os_str().as_bytes();
let mount_point_bytes = mount_point.as_bytes();
let relative_offset = if canonical_bytes.starts_with(mount_point_bytes) {
let off = mount_point_bytes.len();
if off < canonical_bytes.len() && canonical_bytes[off] == b'/' {
off + 1
} else {
off
}
} else {
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "watchos",
target_os = "tvos",
target_os = "visionos",
))]
{
let firmlinked = Path::new(OsStr::from_bytes(mount_point_bytes))
.join(canonical.strip_prefix("/").unwrap_or(&canonical));
if firmlinked.exists() {
1
} else {
canonical_bytes.len()
}
}
#[cfg(not(any(
target_os = "macos",
target_os = "ios",
target_os = "watchos",
target_os = "tvos",
target_os = "visionos",
)))]
{
canonical_bytes.len()
}
};
let is_ejectable = is_ejectable(mount_point.as_path(), device.as_os_str());
Ok(Inner {
mount: super::MountPoint {
mount_point,
device,
is_ejectable,
#[cfg(feature = "disk-usage")]
total_bytes,
#[cfg(feature = "disk-usage")]
available_bytes,
},
canonical,
relative_offset,
})
}
#[cfg(feature = "list")]
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "watchos",
target_os = "tvos",
target_os = "visionos",
))]
pub(super) fn list(opts: super::ListOptions) -> std::io::Result<Vec<super::MountPoint>> {
use objc2_foundation::{
NSArray, NSFileManager, NSURLResourceKey, NSURLVolumeIsBrowsableKey, NSURLVolumeIsEjectableKey,
NSURLVolumeIsLocalKey, NSURLVolumeIsRemovableKey, NSVolumeEnumerationOptions,
};
let fm = NSFileManager::defaultManager();
let keys: &[&NSURLResourceKey] = unsafe {
&[
NSURLVolumeIsBrowsableKey,
NSURLVolumeIsLocalKey,
NSURLVolumeIsEjectableKey,
NSURLVolumeIsRemovableKey,
]
};
let keys_array = NSArray::from_slice(keys);
let urls = fm.mountedVolumeURLsIncludingResourceValuesForKeys_options(
Some(&keys_array),
NSVolumeEnumerationOptions::empty(),
);
let urls = urls.ok_or_else(|| std::io::Error::other("failed to enumerate volumes"))?;
let mut mounts = Vec::new();
for url in urls.iter() {
if !get_bool_resource(&url, unsafe { NSURLVolumeIsBrowsableKey }) {
continue;
}
if !get_bool_resource(&url, unsafe { NSURLVolumeIsLocalKey }) {
continue;
}
let ejectable = get_bool_resource(&url, unsafe { NSURLVolumeIsEjectableKey });
let removable = get_bool_resource(&url, unsafe { NSURLVolumeIsRemovableKey });
let is_ejectable = ejectable || removable;
if opts.is_ejectable_only() && !is_ejectable {
continue;
}
if opts.is_non_ejectable_only() && is_ejectable {
continue;
}
if let Some(path) = url.path() {
let path_bytes = path.to_string().into_bytes();
let mount_point = SmallBytes::from_bytes(&path_bytes);
let mp_path = Path::new(OsStr::from_bytes(&path_bytes));
let fs = match statfs(mp_path) {
Ok(fs) => fs,
Err(_) => continue,
};
let device = SmallBytes::from_bytes(c_chars_as_bytes(&fs.f_mntfromname));
#[cfg(feature = "disk-usage")]
let (total_bytes, available_bytes) = {
let bsize = fs.f_bsize as u64;
(
(fs.f_blocks as u64).saturating_mul(bsize),
(fs.f_bavail as u64).saturating_mul(bsize),
)
};
mounts.push(super::MountPoint {
mount_point,
device,
is_ejectable,
#[cfg(feature = "disk-usage")]
total_bytes,
#[cfg(feature = "disk-usage")]
available_bytes,
});
}
}
Ok(mounts)
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "watchos",
target_os = "tvos",
target_os = "visionos",
))]
pub(super) fn is_ejectable(mount_point: &Path, _device: &OsStr) -> bool {
use objc2_foundation::{NSURL, NSURLVolumeIsEjectableKey, NSURLVolumeIsRemovableKey};
let url = NSURL::fileURLWithPath(&objc2_foundation::NSString::from_str(
&mount_point.to_string_lossy(),
));
let ejectable = get_bool_resource(&url, unsafe { NSURLVolumeIsEjectableKey });
let removable = get_bool_resource(&url, unsafe { NSURLVolumeIsRemovableKey });
ejectable || removable
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "watchos",
target_os = "tvos",
target_os = "visionos",
))]
fn get_bool_resource(
url: &objc2_foundation::NSURL,
key: &objc2_foundation::NSURLResourceKey,
) -> bool {
use objc2_foundation::NSNumber;
let val = url.resourceValuesForKeys_error(&objc2_foundation::NSArray::from_slice(&[key]));
match val {
Ok(dict) => {
let obj = dict.objectForKey(key);
match obj {
Some(obj) => {
let num: &NSNumber = unsafe { &*(&*obj as *const _ as *const NSNumber) };
num.boolValue()
}
None => false,
}
}
Err(_) => false,
}
}
#[cfg(feature = "list")]
#[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "dragonfly"))]
pub(super) fn list(opts: super::ListOptions) -> std::io::Result<Vec<super::MountPoint>> {
const MNT_NOWAIT: core::ffi::c_int = 2;
let mut mntbuf: *mut libc::statfs = core::ptr::null_mut();
let count = unsafe { libc::getmntinfo(&mut mntbuf, MNT_NOWAIT) };
if count <= 0 || mntbuf.is_null() {
return Err(std::io::Error::last_os_error());
}
let entries = unsafe { core::slice::from_raw_parts(mntbuf, count as usize) };
let mut mounts = Vec::new();
for entry in entries {
let fs_type = c_chars_as_bytes(&entry.f_fstypename);
if matches!(
fs_type,
b"autofs" | b"devfs" | b"linprocfs" | b"procfs" | b"fdescfs" | b"tmpfs" | b"linsysfs"
) {
continue;
}
let mp_bytes = c_chars_as_bytes(&entry.f_mntonname);
if mp_bytes == b"/boot/efi" {
continue;
}
let device_bytes = c_chars_as_bytes(&entry.f_mntfromname);
let is_ejectable = is_removable_bsd(fs_type, device_bytes);
if opts.is_ejectable_only() && !is_ejectable {
continue;
}
if opts.is_non_ejectable_only() && is_ejectable {
continue;
}
let mount_point = SmallBytes::from_bytes(mp_bytes);
let device = SmallBytes::from_bytes(device_bytes);
#[cfg(feature = "disk-usage")]
let (total_bytes, available_bytes) = {
let bsize = entry.f_bsize as u64;
(
(entry.f_blocks as u64).saturating_mul(bsize),
(entry.f_bavail as u64).saturating_mul(bsize),
)
};
mounts.push(super::MountPoint {
mount_point,
device,
is_ejectable,
#[cfg(feature = "disk-usage")]
total_bytes,
#[cfg(feature = "disk-usage")]
available_bytes,
});
}
Ok(mounts)
}
#[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "dragonfly"))]
pub(super) fn is_ejectable(mount_point: &Path, _device: &OsStr) -> bool {
match statfs(mount_point) {
Ok(fs) => {
let fs_type = c_chars_as_bytes(&fs.f_fstypename);
let device = c_chars_as_bytes(&fs.f_mntfromname);
is_removable_bsd(fs_type, device)
}
Err(_) => false,
}
}
#[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "dragonfly"))]
fn is_removable_bsd(_fs_type: &[u8], device: &[u8]) -> bool {
device.starts_with(b"/dev/da") || device.starts_with(b"/dev/cd")
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn c_chars_as_bytes(chars: &[core::ffi::c_char]) -> &[u8] {
let bytes: &[u8] =
unsafe { &*(core::ptr::from_ref::<[core::ffi::c_char]>(chars) as *const [u8]) };
let len = super::find_byte(0, bytes).unwrap_or(bytes.len());
&bytes[..len]
}