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::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}