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::fs::OpenOptions;
19use std::io;
20use std::marker::PhantomData;
21use std::os::windows::fs::OpenOptionsExt;
22use std::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle, RawHandle};
23
24use win_shared_os_owned_handle::SharedHandle;
25use win_volume_sys::{DiskExtent, bytes_per_sector, lock_volume, volume_disk_extents};
26
27use super::opened_file::{Buffering, Unbuffered};
28use durable_ioring::{FlushDomain, Geometry};
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.
121#[derive(Clone, Copy, Debug, Default)]
122#[must_use]
123pub struct VolumeOptions {
124 lock: bool,
125}
126
127impl VolumeOptions {
128 /// Nothing requested beyond opening the volume for reading and writing.
129 pub fn new() -> Self {
130 Self::default()
131 }
132
133 /// Lock the volume once it is open, so that this handle is the only one that can use it until
134 /// the handle closes. A file system on the volume is flushed and writes nothing more, which is
135 /// what lets a raw write to its own sectors be made; a volume with no file system needs none.
136 /// Opening fails, and nothing is held, if anything else has the volume open.
137 pub fn lock(mut self, lock: bool) -> Self {
138 self.lock = lock;
139 self
140 }
141}
142
143/// A raw volume this crate opened, in buffering mode `B` ([`Unbuffered`] unless said otherwise): a
144/// [`SharedHandle`], the disks it sits on, the sector it is addressed in and whether it is locked.
145/// Cloning shares the one handle.
146///
147/// It is what [`Win32FileTarget::from_volume`](super::Win32FileTarget::from_volume) is made from.
148#[derive(Clone, Debug)]
149pub struct OpenedVolume<B: Buffering = Unbuffered> {
150 handle: SharedHandle,
151 geometry: Geometry,
152 extents: Vec<DiskExtent>,
153 locked: bool,
154 buffering: PhantomData<B>,
155}
156
157impl<B: Buffering> OpenedVolume<B> {
158 /// Open `path` for reading and writing in buffering mode `B`, find its disks and sector size,
159 /// and lock it if `options` say so.
160 ///
161 /// Opening a volume for writing needs an account that is an administrator.
162 ///
163 /// # Errors
164 ///
165 /// Whatever the open reports, and the device's own error from the extents or sector size
166 /// control: a volume that is remote, or a path that is not a volume, does not answer them, so
167 /// it is refused here. When the lock is asked for, the lock's own error, and then nothing is
168 /// held.
169 pub fn open(path: &VolumePath, options: VolumeOptions) -> io::Result<Self> {
170 let file = OpenOptions::new()
171 .read(true)
172 .write(true)
173 .custom_flags(B::FLAGS)
174 .open(path.as_str())?;
175 let extents = volume_disk_extents(&file)?;
176 let sector = bytes_per_sector(&file)?;
177 let geometry = B::sector_geometry(sector)
178 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
179 if options.lock {
180 lock_volume(&file)?;
181 }
182 Ok(Self {
183 handle: SharedHandle::from(file),
184 geometry,
185 extents,
186 locked: options.lock,
187 buffering: PhantomData,
188 })
189 }
190
191 /// The volume's handle, owned and shared.
192 #[must_use]
193 pub fn handle(&self) -> &SharedHandle {
194 &self.handle
195 }
196
197 /// The alignment its operations need: its sector size.
198 #[must_use]
199 pub fn geometry(&self) -> Geometry {
200 self.geometry
201 }
202
203 /// The runs on disks that make up the volume.
204 #[must_use]
205 pub fn disk_extents(&self) -> &[DiskExtent] {
206 &self.extents
207 }
208
209 /// The flush domains of the volume: one for each disk it sits on, once each, in disk order.
210 #[must_use]
211 pub fn flush_domains(&self) -> Vec<FlushDomain> {
212 domains_of(&self.extents)
213 }
214
215 /// Whether the volume was locked when it was opened.
216 #[must_use]
217 pub fn is_locked(&self) -> bool {
218 self.locked
219 }
220
221 /// A volume over `handle`, with the parts a test gives it, so that what is derived from a
222 /// volume can be tried without opening one, which needs an administrator.
223 #[cfg(test)]
224 pub(crate) fn assembled(
225 handle: SharedHandle,
226 extents: Vec<DiskExtent>,
227 sector: u32,
228 locked: bool,
229 ) -> Self {
230 Self {
231 handle,
232 geometry: B::sector_geometry(sector).expect("a power of two"),
233 extents,
234 locked,
235 buffering: PhantomData,
236 }
237 }
238}
239
240impl<B: Buffering> AsHandle for OpenedVolume<B> {
241 fn as_handle(&self) -> BorrowedHandle<'_> {
242 self.handle.as_handle()
243 }
244}
245
246impl<B: Buffering> AsRawHandle for OpenedVolume<B> {
247 fn as_raw_handle(&self) -> RawHandle {
248 self.handle.as_raw_handle()
249 }
250}
251
252/// One flush domain for each distinct disk in `extents`, in disk order.
253fn domains_of(extents: &[DiskExtent]) -> Vec<FlushDomain> {
254 let mut disks: Vec<u32> = extents.iter().map(|extent| extent.disk_number).collect();
255 disks.sort_unstable();
256 disks.dedup();
257 disks.into_iter().map(disk_flush_domain).collect()
258}