Skip to main content

win_durable_ioring/windows_backend/
opened_disk.rs

1// Copyright (c) 2026 Mike Grier
2//! [`OpenedDisk`]: a whole physical disk this crate opened, addressed directly rather than through
3//! a partition or a volume (DI-D-62).
4//!
5//! Where an [`OpenedVolume`](super::OpenedVolume) is part of a disk, found from the volume's
6//! extents, an `OpenedDisk` is the disk: its path is its number, so which disk it sits on is known
7//! by construction and needs no control to find it, and its one flush domain is that disk's. It is
8//! opened here for the reason a file and a volume are: the warranty a target can claim follows from
9//! how the handle was opened, and nothing documented reports that afterwards.
10//!
11//! Windows documents that it blocks a direct write to a sector of a disk that a mounted volume
12//! covers, so a disk with volumes on it is written through them, or taken offline, or has them
13//! locked first; a disk with no volume on it needs none of that. The tests here use a disk with no
14//! partition and no volume, so that blocking is documented and not exercised by them.
15
16use std::fmt;
17use std::fs::OpenOptions;
18use std::io;
19use std::marker::PhantomData;
20use std::os::windows::fs::OpenOptionsExt;
21use std::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle, RawHandle};
22
23use win_shared_os_owned_handle::SharedHandle;
24use win_volume_sys::bytes_per_sector;
25
26use super::opened_file::{Buffering, Unbuffered};
27use super::opened_volume::disk_flush_domain;
28use durable_ioring::{FlushDomain, Geometry};
29
30#[cfg(test)]
31mod tests;
32
33/// A path naming a physical disk by its number, `\\.\PhysicalDriveN`: the one form that does, so a
34/// path that names a file, a volume or a share cannot be given where a disk is wanted.
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36pub struct DiskPath(u32);
37
38impl DiskPath {
39    /// The disk numbered `disk_number`, as `diskmgmt` and `diskpart` number it.
40    #[must_use]
41    pub fn number(disk_number: u32) -> Self {
42        Self(disk_number)
43    }
44
45    /// The disk's number.
46    #[must_use]
47    pub fn disk_number(&self) -> u32 {
48        self.0
49    }
50
51    /// The path as the system takes it.
52    #[must_use]
53    pub fn path(&self) -> String {
54        format!(r"\\.\PhysicalDrive{}", self.0)
55    }
56}
57
58impl fmt::Display for DiskPath {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        f.write_str(&self.path())
61    }
62}
63
64/// A whole physical disk this crate opened, in buffering mode `B` ([`Unbuffered`] unless said
65/// otherwise): a [`SharedHandle`], its number and the sector it is addressed in. Cloning shares the
66/// one handle.
67///
68/// It is what [`Win32FileTarget::from_disk`](super::Win32FileTarget::from_disk) is made from.
69#[derive(Clone, Debug)]
70pub struct OpenedDisk<B: Buffering = Unbuffered> {
71    handle: SharedHandle,
72    geometry: Geometry,
73    disk_number: u32,
74    buffering: PhantomData<B>,
75}
76
77impl<B: Buffering> OpenedDisk<B> {
78    /// Open `path` for reading and writing in buffering mode `B`, and find its sector size.
79    ///
80    /// Opening a disk for writing needs an account that is an administrator.
81    ///
82    /// # Errors
83    ///
84    /// Whatever the open reports, such as `ERROR_FILE_NOT_FOUND` for a number no disk has, and the
85    /// device's own error from the sector size control.
86    pub fn open(path: DiskPath) -> io::Result<Self> {
87        let file = OpenOptions::new()
88            .read(true)
89            .write(true)
90            .custom_flags(B::FLAGS)
91            .open(path.path())?;
92        let sector = bytes_per_sector(&file)?;
93        let geometry = B::sector_geometry(sector)
94            .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
95        Ok(Self {
96            handle: SharedHandle::from(file),
97            geometry,
98            disk_number: path.disk_number(),
99            buffering: PhantomData,
100        })
101    }
102
103    /// The disk's handle, owned and shared.
104    #[must_use]
105    pub fn handle(&self) -> &SharedHandle {
106        &self.handle
107    }
108
109    /// The alignment its operations need: its sector size.
110    #[must_use]
111    pub fn geometry(&self) -> Geometry {
112        self.geometry
113    }
114
115    /// The disk's number.
116    #[must_use]
117    pub fn disk_number(&self) -> u32 {
118        self.disk_number
119    }
120
121    /// The disk's flush domain: itself.
122    #[must_use]
123    pub fn flush_domain(&self) -> FlushDomain {
124        disk_flush_domain(self.disk_number)
125    }
126
127    /// A disk over `handle`, with the parts a test gives it, so that what is derived from a disk
128    /// can be tried without opening one, which needs an administrator.
129    #[cfg(test)]
130    pub(crate) fn assembled(handle: SharedHandle, disk_number: u32, sector: u32) -> Self {
131        Self {
132            handle,
133            geometry: B::sector_geometry(sector).expect("a power of two"),
134            disk_number,
135            buffering: PhantomData,
136        }
137    }
138}
139
140impl<B: Buffering> AsHandle for OpenedDisk<B> {
141    fn as_handle(&self) -> BorrowedHandle<'_> {
142        self.handle.as_handle()
143    }
144}
145
146impl<B: Buffering> AsRawHandle for OpenedDisk<B> {
147    fn as_raw_handle(&self) -> RawHandle {
148        self.handle.as_raw_handle()
149    }
150}