#[cfg(all(target_os = "linux", not(target_env = "musl")))]
use std::{ffi::CString, mem};
#[cfg(target_os = "linux")]
use std::{fs, path::PathBuf};
use std::{io, path::Path};
#[derive(Debug, PartialEq, Eq)]
pub enum DirectIoSupport {
Unsupported,
Supported,
Uncertain,
}
#[cfg(target_os = "linux")]
pub fn check_direct_io_capability(path: impl AsRef<Path>) -> io::Result<DirectIoSupport> {
let path = path.as_ref();
if let Some(file) = find_any_file_under_path(path) {
check_direct_io_for_file(&file)
} else {
if !path.is_dir() {
return Ok(DirectIoSupport::Uncertain);
}
let Ok(tmp) = tempfile::NamedTempFile::new_in(path) else {
return Ok(DirectIoSupport::Uncertain);
};
let result = check_direct_io_for_file(tmp.path());
tmp.close()?;
result
}
}
#[cfg(target_os = "linux")]
fn check_direct_io_for_file(file: &Path) -> io::Result<DirectIoSupport> {
#[cfg(not(target_env = "musl"))]
{
let statx_result = check_direct_io_via_statx(file);
if !matches!(&statx_result, Ok(DirectIoSupport::Uncertain)) {
return statx_result;
}
}
Ok(check_direct_io_via_open_probe(file))
}
#[cfg(not(target_os = "linux"))]
pub fn check_direct_io_capability(_path: impl AsRef<Path>) -> io::Result<DirectIoSupport> {
Ok(DirectIoSupport::Uncertain)
}
#[cfg(all(target_os = "linux", not(target_env = "musl")))]
fn check_direct_io_via_statx(path: &Path) -> io::Result<DirectIoSupport> {
let path_cstr = CString::new(path.as_os_str().as_encoded_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains a null byte"))?;
let mut statx_info: libc::statx = unsafe { mem::zeroed() };
let ret = unsafe {
libc::statx(
libc::AT_FDCWD,
path_cstr.as_ptr(),
libc::AT_NO_AUTOMOUNT,
libc::STATX_DIOALIGN,
&mut statx_info,
)
};
if ret != 0 {
return Err(io::Error::last_os_error());
}
let supported = if statx_info.stx_mask & libc::STATX_DIOALIGN == 0 {
DirectIoSupport::Uncertain
} else if statx_info.stx_dio_mem_align != 0 {
DirectIoSupport::Supported
} else {
DirectIoSupport::Unsupported
};
Ok(supported)
}
#[cfg(target_os = "linux")]
fn check_direct_io_via_open_probe(file: &Path) -> DirectIoSupport {
use std::{fs::OpenOptions, os::unix::fs::OpenOptionsExt as _};
if OpenOptions::new().read(true).open(file).is_err() {
return DirectIoSupport::Uncertain;
}
if OpenOptions::new()
.read(true)
.custom_flags(libc::O_DIRECT)
.open(file)
.is_ok()
{
DirectIoSupport::Supported
} else {
DirectIoSupport::Unsupported
}
}
#[cfg(target_os = "linux")]
fn find_any_file_under_path(path: &Path) -> Option<PathBuf> {
if path.is_file() {
return Some(path.to_path_buf());
}
if !path.is_dir() {
return None;
}
let entries = match fs::read_dir(path) {
Ok(entries) => entries,
Err(err) => {
log::warn!(
"skipping `{}` while probing direct-io support: {err}",
path.display()
);
return None;
}
};
for entry in entries.flatten() {
if let Some(found) = find_any_file_under_path(&entry.path()) {
return Some(found);
}
}
None
}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use {super::*, tempfile::TempDir};
fn make_temp_file(dir: &TempDir, name: &str, content: &[u8]) -> std::path::PathBuf {
let path = dir.path().join(name);
std::fs::write(&path, content).unwrap();
path
}
enum UnreadableDirTest<R> {
SkippedAsRoot,
Ran(R),
}
fn with_unreadable_dir<R>(
path: &std::path::Path,
f: impl FnOnce() -> R,
) -> UnreadableDirTest<R> {
use std::os::unix::fs::PermissionsExt;
if unsafe { libc::geteuid() } == 0 {
return UnreadableDirTest::SkippedAsRoot;
}
std::fs::create_dir(path).unwrap();
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o000)).unwrap();
let result = f();
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700)).unwrap();
UnreadableDirTest::Ran(result)
}
#[test]
fn test_find_any_file_under_path_file() {
let dir = TempDir::new().unwrap();
let file = make_temp_file(&dir, "f.bin", b"hello");
assert_eq!(find_any_file_under_path(&file), Some(file));
}
#[test]
fn test_find_any_file_under_path_dir() {
let dir = TempDir::new().unwrap();
make_temp_file(&dir, "a.bin", b"data");
let candidate = find_any_file_under_path(dir.path());
assert!(candidate.is_some());
assert!(candidate.unwrap().is_file());
}
#[test]
fn test_find_any_file_under_path_empty_dir() {
let dir = TempDir::new().unwrap();
assert_eq!(find_any_file_under_path(dir.path()), None);
}
#[test]
fn test_find_any_file_under_path_skips_unreadable_subdir() {
let dir = TempDir::new().unwrap();
let file = make_temp_file(&dir, "snapshot.bin", b"data");
let unreadable = dir.path().join("lost+found");
if let UnreadableDirTest::Ran(found) =
with_unreadable_dir(&unreadable, || find_any_file_under_path(dir.path()))
{
assert_eq!(found, Some(file));
}
}
#[test]
fn test_find_any_file_under_path_readable_dir_only_unreadable_subdir() {
let dir = TempDir::new().unwrap();
let unreadable = dir.path().join("lost+found");
if let UnreadableDirTest::Ran(found) =
with_unreadable_dir(&unreadable, || find_any_file_under_path(dir.path()))
{
assert_eq!(found, None);
}
}
#[test]
fn test_find_any_file_under_path_unreadable_path_returns_none() {
let parent = TempDir::new().unwrap();
let dir = parent.path().join("accounts");
if let UnreadableDirTest::Ran(found) =
with_unreadable_dir(&dir, || find_any_file_under_path(&dir))
{
assert_eq!(found, None);
}
}
#[test]
fn test_path_supports_direct_io_file() {
let dir = TempDir::new().unwrap();
let file = make_temp_file(&dir, "probe.bin", &[0u8; 4096]);
let result = check_direct_io_capability(&file).expect("check must not fail");
assert_eq!(
result,
DirectIoSupport::Supported,
"dev filesystem must support direct I/O"
);
}
#[test]
fn test_path_supports_direct_io_dir() {
let dir = TempDir::new().unwrap();
make_temp_file(&dir, "probe.bin", &[0u8; 4096]);
let result = check_direct_io_capability(dir.path()).expect("check must not fail");
assert_eq!(
result,
DirectIoSupport::Supported,
"dev filesystem must support direct I/O"
);
}
#[test]
fn test_path_supports_direct_io_empty_dir() {
let dir = TempDir::new().unwrap();
let result = check_direct_io_capability(dir.path()).expect("check must not fail");
assert_eq!(
result,
DirectIoSupport::Supported,
"dev filesystem must support direct I/O"
);
}
#[test]
fn test_path_supports_direct_io_nonexistent_path() {
let dir = TempDir::new().unwrap();
let result = check_direct_io_capability(dir.path().join("does-not-exist"))
.expect("check must not fail");
assert_eq!(result, DirectIoSupport::Uncertain);
}
}