Skip to main content

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}