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}