odirect 0.5.0

Minimal cross-platform direct I/O abstraction for Rust.
Documentation
use std::fs::File;
use std::fs::OpenOptions;

#[cfg(target_os = "windows")]
#[allow(dead_code)]
use std::os::windows::fs::OpenOptionsExt;

#[cfg(target_os = "macos")]
#[allow(dead_code)]
use std::os::unix::io::AsRawFd;

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
#[allow(dead_code)]
use std::os::unix::fs::OpenOptionsExt;

#[allow(dead_code)]
pub enum AccessMode {
    Read,
    Write,
    ReadWrite,
}

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
#[allow(dead_code)]
pub fn open_direct_file(
    path: &str,
    mode: AccessMode,
) -> std::io::Result<File> {
    let mut opts = OpenOptions::new();

    match mode {
        AccessMode::Read => {
            opts.read(true);
        }
        AccessMode::Write => {
            opts.write(true);
        }
        AccessMode::ReadWrite => {
            opts.read(true).write(true);
        }
    }

    opts.custom_flags(libc::O_DIRECT).open(path)
}

#[cfg(target_os = "macos")]
#[allow(dead_code)]
pub fn open_direct_file(
    path: &str,
    mode: AccessMode,
) -> std::io::Result<File> {
    let mut opts = OpenOptions::new();

    match mode {
        AccessMode::Read => {
            opts.read(true);
        }
        AccessMode::Write => {
            opts.write(true);
        }
        AccessMode::ReadWrite => {
            opts.read(true).write(true);
        }
    }

    let file = opts.open(path)?;

    unsafe {
        let fd = file.as_raw_fd();
        libc::fcntl(fd, libc::F_NOCACHE, 1);
    }

    Ok(file)
}

#[cfg(target_os = "windows")]
#[allow(dead_code)]
pub fn open_direct_file(
    path: &str,
    mode: AccessMode,
) -> std::io::Result<File> {
    const O_DIRECT: u32 = 0x20000000;
    let mut opts = OpenOptions::new();

    match mode {
        AccessMode::Read => {
            opts.read(true);
        }
        AccessMode::Write => {
            opts.write(true);
        }
        AccessMode::ReadWrite => {
            opts.read(true).write(true);
        }
    }

    opts.custom_flags(O_DIRECT).open(path)
}

#[cfg(test)]
mod tests {
    use super::*;

    use std::fs::File;
    use std::io::Write;
    use std::time::{SystemTime, UNIX_EPOCH};

    fn unique_file(name: &str) -> String {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();

        format!("{}_{}.tmp", name, nanos)
    }

    fn create_test_file(path: &str) {
        let mut file = File::create(path).unwrap();
        writeln!(file, "hello").unwrap();
    }

    fn cleanup(path: &str) {
        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn open_read_mode() {
        let path = unique_file("read_mode");
        create_test_file(&path);

        assert!(open_direct_file(&path, AccessMode::Read).is_ok());

        cleanup(&path);
    }

    #[test]
    fn open_write_mode() {
        let path = unique_file("write_mode");
        create_test_file(&path);

        assert!(open_direct_file(&path, AccessMode::Write).is_ok());

        cleanup(&path);
    }

    #[test]
    fn open_readwrite_mode() {
        let path = unique_file("readwrite_mode");
        create_test_file(&path);

        assert!(open_direct_file(&path, AccessMode::ReadWrite).is_ok());

        cleanup(&path);
    }

    #[test]
    fn open_nonexistent_file_fails() {
        let path = unique_file("missing_file");

        assert!(open_direct_file(&path, AccessMode::Read).is_err());
    }

    #[test]
    fn multiple_open_calls() {
        let path = unique_file("multiple");
        create_test_file(&path);

        for _ in 0..50 {
            assert!(open_direct_file(&path, AccessMode::ReadWrite).is_ok());
        }

        cleanup(&path);
    }

    #[test]
    fn sequential_modes() {
        let path = unique_file("sequential");
        create_test_file(&path);

        assert!(open_direct_file(&path, AccessMode::Read).is_ok());
        assert!(open_direct_file(&path, AccessMode::Write).is_ok());
        assert!(open_direct_file(&path, AccessMode::ReadWrite).is_ok());

        cleanup(&path);
    }
}