use crate::{DiskError, Result};
use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::fd::AsRawFd;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use bootsmith_core::Device;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum OpenMode {
ReadOnly,
ReadWrite,
}
pub struct RawDevice {
file: File,
path: PathBuf,
size_bytes: u64,
block_size: u32,
label: String,
}
impl RawDevice {
pub fn open(path: impl AsRef<Path>, mode: OpenMode, model_label: &str) -> Result<Self> {
let path = path.as_ref().to_path_buf();
let path_str = path.to_string_lossy().into_owned();
if !path_str.starts_with("/dev/rdisk") {
return Err(DiskError::BadDevicePath(path_str));
}
let mut opts = OpenOptions::new();
opts.read(true);
if mode == OpenMode::ReadWrite {
opts.write(true);
}
opts.custom_flags(nix::libc::O_SYNC);
let file = opts.open(&path).map_err(DiskError::Io)?;
set_fnocache(&file)?;
let (block_size, block_count) = block_geometry(&file, &path_str)?;
let size_bytes = block_size as u64 * block_count;
Ok(Self {
file,
path,
size_bytes,
block_size,
label: format!("{path_str} ({model_label})"),
})
}
pub fn block_size(&self) -> u32 {
self.block_size
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl Device for RawDevice {
fn size_bytes(&self) -> bootsmith_core::Result<u64> {
Ok(self.size_bytes)
}
fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> bootsmith_core::Result<()> {
self.file
.seek(SeekFrom::Start(offset))
.map_err(bootsmith_core::Error::Io)?;
self.file.read_exact(buf).map_err(bootsmith_core::Error::Io)
}
fn write_at(&mut self, offset: u64, buf: &[u8]) -> bootsmith_core::Result<()> {
self.file
.seek(SeekFrom::Start(offset))
.map_err(bootsmith_core::Error::Io)?;
self.file.write_all(buf).map_err(bootsmith_core::Error::Io)
}
fn sync(&mut self) -> bootsmith_core::Result<()> {
self.file.flush().map_err(bootsmith_core::Error::Io)?;
let fd = self.file.as_raw_fd();
let rc = unsafe { nix::libc::fsync(fd) };
if rc != 0 {
return Err(bootsmith_core::Error::Io(std::io::Error::last_os_error()));
}
Ok(())
}
fn describe(&self) -> String {
self.label.clone()
}
}
fn set_fnocache(file: &File) -> Result<()> {
let fd = file.as_raw_fd();
const F_NOCACHE: nix::libc::c_int = 48;
let rc = unsafe { nix::libc::fcntl(fd, F_NOCACHE, 1 as nix::libc::c_int) };
if rc < 0 {
let err = std::io::Error::last_os_error();
return Err(DiskError::DaError(format!("fcntl(F_NOCACHE): {err}")));
}
Ok(())
}
fn block_geometry(file: &File, label: &str) -> Result<(u32, u64)> {
let fd = file.as_raw_fd();
const DKIOCGETBLOCKSIZE: nix::libc::c_ulong = 0x40046418;
const DKIOCGETBLOCKCOUNT: nix::libc::c_ulong = 0x40086419;
let mut block_size: u32 = 0;
let rc = unsafe {
nix::libc::ioctl(fd, DKIOCGETBLOCKSIZE, &mut block_size as *mut u32)
};
if rc != 0 {
let err = std::io::Error::last_os_error();
return Err(DiskError::DaError(format!(
"DKIOCGETBLOCKSIZE on {label}: {err}"
)));
}
let mut block_count: u64 = 0;
let rc = unsafe {
nix::libc::ioctl(fd, DKIOCGETBLOCKCOUNT, &mut block_count as *mut u64)
};
if rc != 0 {
let err = std::io::Error::last_os_error();
return Err(DiskError::DaError(format!(
"DKIOCGETBLOCKCOUNT on {label}: {err}"
)));
}
Ok((block_size, block_count))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_rejects_buffered_device_path() {
match RawDevice::open("/dev/disk8", OpenMode::ReadOnly, "test") {
Err(DiskError::BadDevicePath(p)) => assert_eq!(p, "/dev/disk8"),
Err(other) => panic!("wrong error: {other:?}"),
Ok(_) => panic!("expected BadDevicePath rejection"),
}
}
#[test]
fn open_rejects_non_dev_path() {
match RawDevice::open("/tmp/notadevice", OpenMode::ReadOnly, "test") {
Err(DiskError::BadDevicePath(_)) => {}
Err(other) => panic!("wrong error: {other:?}"),
Ok(_) => panic!("expected BadDevicePath rejection"),
}
}
}