win_durable_ioring/windows_backend/opened_volume.rs
1// Copyright (c) 2026 Mike Grier
2//! [`OpenedVolume`]: a raw volume this crate opened, so how it was opened, which disks it sits on
3//! and the sector it is addressed in are known by construction (DI-D-61).
4//!
5//! A volume is a target whose regions are offsets the consumer allocates inside it: one handle, one
6//! commit per volume, and every region shares the volume's flush domains. Those are found from the
7//! volume's disks, which is this backing's own obligation (the contract's "Complete flush-domain
8//! declarations"), by a documented device control, not declared by the consumer.
9//!
10//! The volume is opened here, by path, for the reason [`OpenedFile`](super::OpenedFile) is: the
11//! warranty a target can claim follows from how the handle was opened, and nothing documented reports
12//! it afterwards. Only the forms of path that name a volume are accepted, a drive letter or a volume
13//! GUID, and a volume that does not answer the extents control -- a remote one, or not a volume --
14//! is refused, so a [`Win32FileTarget`](super::Win32FileTarget) over one is over this machine's own
15//! disks.
16
17use std::fmt;
18use std::io;
19use std::marker::PhantomData;
20use std::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle, RawHandle};
21
22use win_shared_os_owned_handle::SharedHandle;
23use win_volume_sys::{DiskExtent, bytes_per_sector, lock_volume, volume_disk_extents};
24
25use super::opened_file::{Buffering, Unbuffered};
26use super::raw_open;
27use super::share_mode::ShareMode;
28use durable_ioring::{DataAccess, FlushDomain, Geometry, ReadWrite};
29
30#[cfg(test)]
31mod tests;
32
33/// The flush domain of disk `disk_number`: `\\.\PhysicalDriveN`, the name a disk answers to.
34///
35/// A raw volume derives one for each disk it sits on. A consumer declares the same for a file on
36/// that disk (`Win32FileTarget::with_domains`), so a commit that fails on the volume reaches the
37/// file's writes and the other way round: two targets share a domain exactly when their bytes
38/// agree.
39#[must_use]
40pub fn disk_flush_domain(disk_number: u32) -> FlushDomain {
41 FlushDomain::new(format!(r"\\.\PhysicalDrive{disk_number}").into_bytes())
42}
43
44/// A path naming a volume, in one of the two forms that do: a drive letter, `\\.\C:`, or a volume
45/// GUID, `\\?\Volume{...}`. Nothing else is one, so a path that names a file, a directory or a
46/// share cannot be given where a volume is wanted.
47#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct VolumePath(String);
49
50/// A volume path was asked for in a form that does not name a volume.
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct VolumePathError(String);
53
54impl fmt::Display for VolumePathError {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(
57 f,
58 "{:?} is not a drive letter or a volume GUID path",
59 self.0
60 )
61 }
62}
63
64impl std::error::Error for VolumePathError {}
65
66/// The text of a volume GUID path before the GUID, and after it.
67const GUID_PATH_PREFIX: &str = r"\\?\Volume{";
68const GUID_PATH_SUFFIX: char = '}';
69
70/// The lengths of a GUID's five groups of hexadecimal digits, which dashes separate.
71const GUID_GROUPS: [usize; 5] = [8, 4, 4, 4, 12];
72
73impl VolumePath {
74 /// The volume mounted at drive letter `letter`, `A` to `Z` in either case.
75 ///
76 /// # Errors
77 ///
78 /// [`VolumePathError`] for any other character.
79 pub fn drive(letter: char) -> Result<Self, VolumePathError> {
80 if letter.is_ascii_alphabetic() {
81 Ok(Self(format!(r"\\.\{}:", letter.to_ascii_uppercase())))
82 } else {
83 Err(VolumePathError(letter.to_string()))
84 }
85 }
86
87 /// The volume named by a volume GUID path, such as `\\?\Volume{01234567-89ab-cdef-0123-456789abcdef}`
88 /// or the same with the trailing backslash the enumeration functions give it, which is dropped:
89 /// a volume handle is opened without it.
90 ///
91 /// # Errors
92 ///
93 /// [`VolumePathError`] for any other form, including a GUID of the wrong shape.
94 pub fn guid(path: &str) -> Result<Self, VolumePathError> {
95 let refused = || VolumePathError(path.to_owned());
96 let path = path.strip_suffix('\\').unwrap_or(path);
97 let inner = path
98 .strip_prefix(GUID_PATH_PREFIX)
99 .and_then(|rest| rest.strip_suffix(GUID_PATH_SUFFIX))
100 .ok_or_else(refused)?;
101 let groups: Vec<&str> = inner.split('-').collect();
102 let shaped = groups.len() == GUID_GROUPS.len()
103 && groups.iter().zip(GUID_GROUPS).all(|(group, length)| {
104 group.len() == length && group.chars().all(|c| c.is_ascii_hexdigit())
105 });
106 if shaped {
107 Ok(Self(path.to_owned()))
108 } else {
109 Err(refused())
110 }
111 }
112
113 /// The path as the system takes it.
114 #[must_use]
115 pub fn as_str(&self) -> &str {
116 &self.0
117 }
118}
119
120/// How a volume is opened beyond its path: whether it is locked, and what other opens of the volume
121/// may request meanwhile.
122#[derive(Clone, Copy, Debug)]
123#[must_use]
124pub struct VolumeOptions {
125 lock: bool,
126 share: ShareMode,
127}
128
129impl Default for VolumeOptions {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135impl VolumeOptions {
136 /// Nothing requested beyond opening the volume, which is shared for reading, writing and
137 /// deletion, as `std::fs::OpenOptions` opens one.
138 pub fn new() -> Self {
139 Self {
140 lock: false,
141 share: ShareMode::ALL,
142 }
143 }
144
145 /// What other opens of the volume may request while this handle is open (`dwShareMode`), as
146 /// [`FileOptions::share`](super::FileOptions::share) is for a file. [`ShareMode::NONE`] opens the
147 /// volume exclusively. An open the sharing of either side does not allow fails with a
148 /// [`SharingViolation`](super::SharingViolation). Calling this again replaces the mode.
149 ///
150 /// The `CreateFileW` page says that "When opening a volume or floppy disk, the *dwShareMode*
151 /// parameter must have the `FILE_SHARE_WRITE` flag" (read 2026-10-18). A mode without it is not
152 /// refused here, because the measurement contradicts the page: on Windows 11 build 26300, elevated,
153 /// an exclusive open of a volume succeeded and refused a second open until it closed (the test
154 /// `an_exclusive_raw_volume_refuses_a_second_open_until_it_closes`). Where the documentation is
155 /// shown to be wrong, this crate does not build the documented condition in as a refusal.
156 ///
157 /// This is not the lock: [`lock`](Self::lock) is a device control made once the volume is open,
158 /// and the share mode is judged by the open itself.
159 pub fn share(mut self, share: ShareMode) -> Self {
160 self.share = share;
161 self
162 }
163
164 /// Lock the volume once it is open, so that this handle is the only one that can use it until
165 /// the handle closes. A file system on the volume is flushed and writes nothing more, which is
166 /// what lets a raw write to its own sectors be made; a volume with no file system needs none.
167 /// Opening fails, and nothing is held, if anything else has the volume open.
168 ///
169 /// The `FSCTL_LOCK_VOLUME` page names no right the handle needs, so the lock is offered with any
170 /// data access and the kernel's error is the answer if one is needed; which it is was not
171 /// measured, since it needs an administrator.
172 pub fn lock(mut self, lock: bool) -> Self {
173 self.lock = lock;
174 self
175 }
176}
177
178/// A raw volume this crate opened, in buffering mode `B` ([`Unbuffered`] unless said otherwise) and
179/// with data access `A` ([`ReadWrite`] unless said otherwise): a [`SharedHandle`], the disks it sits
180/// on, the sector it is addressed in and whether it is locked. Cloning shares the one handle.
181///
182/// It is what [`Win32FileTarget::from_volume`](super::Win32FileTarget::from_volume) is made from, when
183/// `A` has a data right. The data access is the one the open was given, which `options` do not carry,
184/// so it is named in the type at the call: `OpenedVolume::<Unbuffered, ReadOnly>::open(..)`.
185#[derive(Clone, Debug)]
186pub struct OpenedVolume<B: Buffering = Unbuffered, A: DataAccess = ReadWrite> {
187 handle: SharedHandle,
188 geometry: Geometry,
189 extents: Vec<DiskExtent>,
190 locked: bool,
191 mode: PhantomData<(B, A)>,
192}
193
194impl<B: Buffering, A: DataAccess> OpenedVolume<B, A> {
195 /// Open `path` with data access `A` in buffering mode `B`, find its disks and sector size, and
196 /// lock it if `options` say so.
197 ///
198 /// Opening a volume for writing needs an account that is an administrator. Opening it with
199 /// [`NoData`](super::NoData) asks for no data right, which is enough to query the volume, and the
200 /// result cannot be made into a target.
201 ///
202 /// # Errors
203 ///
204 /// Whatever the open reports, a [`SharingViolation`](super::SharingViolation) among it, and the
205 /// device's own error from the extents or sector size control: a volume that is remote, or a path that is not a volume, does not answer them, so
206 /// it is refused here. When the lock is asked for, the lock's own error, and then nothing is
207 /// held.
208 pub fn open(path: &VolumePath, options: VolumeOptions) -> io::Result<Self> {
209 let file = raw_open::open::<B, A>(path.as_str(), options.share)?;
210 let extents = volume_disk_extents(&file)?;
211 let sector = bytes_per_sector(&file)?;
212 let geometry = B::sector_geometry(sector)
213 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
214 if options.lock {
215 lock_volume(&file)?;
216 }
217 Ok(Self {
218 handle: SharedHandle::from(file),
219 geometry,
220 extents,
221 locked: options.lock,
222 mode: PhantomData,
223 })
224 }
225
226 /// The volume's handle, owned and shared.
227 #[must_use]
228 pub fn handle(&self) -> &SharedHandle {
229 &self.handle
230 }
231
232 /// The alignment its operations need: its sector size.
233 #[must_use]
234 pub fn geometry(&self) -> Geometry {
235 self.geometry
236 }
237
238 /// The runs on disks that make up the volume.
239 #[must_use]
240 pub fn disk_extents(&self) -> &[DiskExtent] {
241 &self.extents
242 }
243
244 /// The flush domains of the volume: one for each disk it sits on, once each, in disk order.
245 #[must_use]
246 pub fn flush_domains(&self) -> Vec<FlushDomain> {
247 domains_of(&self.extents)
248 }
249
250 /// Whether the volume was locked when it was opened.
251 #[must_use]
252 pub fn is_locked(&self) -> bool {
253 self.locked
254 }
255
256 /// A volume over `handle`, with the parts a test gives it, so that what is derived from a
257 /// volume can be tried without opening one, which needs an administrator.
258 #[cfg(test)]
259 pub(crate) fn assembled(
260 handle: SharedHandle,
261 extents: Vec<DiskExtent>,
262 sector: u32,
263 locked: bool,
264 ) -> Self {
265 Self {
266 handle,
267 geometry: B::sector_geometry(sector).expect("a power of two"),
268 extents,
269 locked,
270 mode: PhantomData,
271 }
272 }
273}
274
275impl<B: Buffering, A: DataAccess> AsHandle for OpenedVolume<B, A> {
276 fn as_handle(&self) -> BorrowedHandle<'_> {
277 self.handle.as_handle()
278 }
279}
280
281impl<B: Buffering, A: DataAccess> AsRawHandle for OpenedVolume<B, A> {
282 fn as_raw_handle(&self) -> RawHandle {
283 self.handle.as_raw_handle()
284 }
285}
286
287/// One flush domain for each distinct disk in `extents`, in disk order.
288fn domains_of(extents: &[DiskExtent]) -> Vec<FlushDomain> {
289 let mut disks: Vec<u32> = extents.iter().map(|extent| extent.disk_number).collect();
290 disks.sort_unstable();
291 disks.dedup();
292 disks.into_iter().map(disk_flush_domain).collect()
293}