use std::fmt;
use std::fs::OpenOptions;
use std::io;
use std::marker::PhantomData;
use std::os::windows::fs::OpenOptionsExt;
use std::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle, RawHandle};
use win_shared_os_owned_handle::SharedHandle;
use win_volume_sys::{DiskExtent, bytes_per_sector, lock_volume, volume_disk_extents};
use super::opened_file::{Buffering, Unbuffered};
use durable_ioring::{FlushDomain, Geometry};
#[cfg(test)]
mod tests;
#[must_use]
pub fn disk_flush_domain(disk_number: u32) -> FlushDomain {
FlushDomain::new(format!(r"\\.\PhysicalDrive{disk_number}").into_bytes())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VolumePath(String);
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VolumePathError(String);
impl fmt::Display for VolumePathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:?} is not a drive letter or a volume GUID path",
self.0
)
}
}
impl std::error::Error for VolumePathError {}
const GUID_PATH_PREFIX: &str = r"\\?\Volume{";
const GUID_PATH_SUFFIX: char = '}';
const GUID_GROUPS: [usize; 5] = [8, 4, 4, 4, 12];
impl VolumePath {
pub fn drive(letter: char) -> Result<Self, VolumePathError> {
if letter.is_ascii_alphabetic() {
Ok(Self(format!(r"\\.\{}:", letter.to_ascii_uppercase())))
} else {
Err(VolumePathError(letter.to_string()))
}
}
pub fn guid(path: &str) -> Result<Self, VolumePathError> {
let refused = || VolumePathError(path.to_owned());
let path = path.strip_suffix('\\').unwrap_or(path);
let inner = path
.strip_prefix(GUID_PATH_PREFIX)
.and_then(|rest| rest.strip_suffix(GUID_PATH_SUFFIX))
.ok_or_else(refused)?;
let groups: Vec<&str> = inner.split('-').collect();
let shaped = groups.len() == GUID_GROUPS.len()
&& groups.iter().zip(GUID_GROUPS).all(|(group, length)| {
group.len() == length && group.chars().all(|c| c.is_ascii_hexdigit())
});
if shaped {
Ok(Self(path.to_owned()))
} else {
Err(refused())
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Copy, Debug, Default)]
#[must_use]
pub struct VolumeOptions {
lock: bool,
}
impl VolumeOptions {
pub fn new() -> Self {
Self::default()
}
pub fn lock(mut self, lock: bool) -> Self {
self.lock = lock;
self
}
}
#[derive(Clone, Debug)]
pub struct OpenedVolume<B: Buffering = Unbuffered> {
handle: SharedHandle,
geometry: Geometry,
extents: Vec<DiskExtent>,
locked: bool,
buffering: PhantomData<B>,
}
impl<B: Buffering> OpenedVolume<B> {
pub fn open(path: &VolumePath, options: VolumeOptions) -> io::Result<Self> {
let file = OpenOptions::new()
.read(true)
.write(true)
.custom_flags(B::FLAGS)
.open(path.as_str())?;
let extents = volume_disk_extents(&file)?;
let sector = bytes_per_sector(&file)?;
let geometry = B::sector_geometry(sector)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
if options.lock {
lock_volume(&file)?;
}
Ok(Self {
handle: SharedHandle::from(file),
geometry,
extents,
locked: options.lock,
buffering: PhantomData,
})
}
#[must_use]
pub fn handle(&self) -> &SharedHandle {
&self.handle
}
#[must_use]
pub fn geometry(&self) -> Geometry {
self.geometry
}
#[must_use]
pub fn disk_extents(&self) -> &[DiskExtent] {
&self.extents
}
#[must_use]
pub fn flush_domains(&self) -> Vec<FlushDomain> {
domains_of(&self.extents)
}
#[must_use]
pub fn is_locked(&self) -> bool {
self.locked
}
#[cfg(test)]
pub(crate) fn assembled(
handle: SharedHandle,
extents: Vec<DiskExtent>,
sector: u32,
locked: bool,
) -> Self {
Self {
handle,
geometry: B::sector_geometry(sector).expect("a power of two"),
extents,
locked,
buffering: PhantomData,
}
}
}
impl<B: Buffering> AsHandle for OpenedVolume<B> {
fn as_handle(&self) -> BorrowedHandle<'_> {
self.handle.as_handle()
}
}
impl<B: Buffering> AsRawHandle for OpenedVolume<B> {
fn as_raw_handle(&self) -> RawHandle {
self.handle.as_raw_handle()
}
}
fn domains_of(extents: &[DiskExtent]) -> Vec<FlushDomain> {
let mut disks: Vec<u32> = extents.iter().map(|extent| extent.disk_number).collect();
disks.sort_unstable();
disks.dedup();
disks.into_iter().map(disk_flush_domain).collect()
}