Skip to main content

win_durable_ioring/windows_backend/
opened_file.rs

1// Copyright (c) 2026 Mike Grier
2//! [`OpenedFile`]: a file this crate opened, so how it was opened is known by construction
3//! (DI-D-60).
4//!
5//! A [`Win32FileTarget`](super::Win32FileTarget) states a warranty class, an alignment and what it
6//! offers, and every one of them depends on how its file was opened: no intermediate buffering
7//! is what lets a file claim [`WarrantyClass::SharedDevice`], and what makes its operations need a
8//! sector's alignment. A handle someone else opened cannot be asked, because the only call that
9//! reports the open mode is undocumented, which this repository does not call. So the file is
10//! opened here, by a type that records what it did, and a target can be made from nothing else.
11//!
12//! The buffering mode is a type: [`OpenedFile<Cached>`] or [`OpenedFile<Unbuffered>`]. What a mode
13//! implies is stated once, on [`Buffering`]; a consumer that needs the stronger warranty asks for
14//! `OpenedFile<Unbuffered>` in its own signature, and a cached file cannot be passed.
15
16use std::fmt;
17use std::fs;
18use std::io;
19use std::marker::PhantomData;
20use std::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle, RawHandle};
21use std::path::{Component, Path, PathBuf, Prefix};
22
23use win_shared_os_owned_handle::SharedHandle;
24use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE};
25use windows_sys::Win32::Storage::FileSystem::{
26    CREATE_NEW, FILE_FLAG_OPEN_REPARSE_POINT, OPEN_ALWAYS, OPEN_EXISTING, TRUNCATE_EXISTING,
27};
28
29use durable_ioring::{Capabilities, Geometry, GeometryError, WarrantyClass};
30
31mod open_request;
32
33#[cfg(test)]
34mod tests;
35
36/// `FILE_FLAG_NO_BUFFERING`, as `CreateFileW` documents it: the file is opened without the system
37/// cache, so every transfer needs the device's alignment.
38const FILE_FLAG_NO_BUFFERING: u32 = 0x2000_0000;
39
40mod sealed {
41    pub trait Sealed {}
42}
43
44/// How a file is buffered, which decides what a target made from it can claim. Sealed: there are
45/// two modes, [`Cached`] and [`Unbuffered`], and a warranty is a statement about each of them that
46/// this crate makes.
47pub trait Buffering: sealed::Sealed + Copy + fmt::Debug + Send + Sync + 'static {
48    /// The `CreateFileW` flags this mode opens with.
49    const FLAGS: u32;
50
51    /// The warranty class a file in this mode can claim.
52    const WARRANTY: WarrantyClass;
53
54    /// What a file in this mode offers beyond the contract's obligations.
55    const CAPABILITIES: Capabilities;
56
57    /// Whether the claim in [`WARRANTY`](Self::WARRANTY) holds only on a device of this machine, so
58    /// that a file on a remote one is refused rather than given a class it cannot keep.
59    const LOCAL_DEVICE_ONLY: bool;
60
61    /// The alignment a file in this mode starts with.
62    fn geometry() -> Geometry;
63
64    /// The alignment a raw volume or disk in this mode needs, addressed in sectors of `sector_size`
65    /// bytes. Its transfers are whole sectors in either mode, so offsets and lengths are aligned to
66    /// one; its buffers only when it is unbuffered, as the cache is what lets a cached transfer
67    /// start anywhere.
68    ///
69    /// # Errors
70    ///
71    /// [`GeometryError`] if `sector_size` is not a power of two.
72    fn sector_geometry(sector_size: u32) -> Result<Geometry, GeometryError>;
73}
74
75/// A file opened for cached I/O: the system cache holds its writes, and its write-back can fail
76/// without telling the writer, so it claims [`WarrantyClass::BestEffort`] and needs no alignment.
77#[derive(Clone, Copy, Debug)]
78pub struct Cached;
79
80/// A file opened with no intermediate buffering: its transfers reach the device, so it can claim
81/// [`WarrantyClass::SharedDevice`], and need the device's sector alignment. Per-write
82/// write-through is honoured only on such a file, which the kernel refuses on a cached one
83/// (error 509, measured 2026-10-07 and kept as a test), so it offers that capability.
84#[derive(Clone, Copy, Debug)]
85pub struct Unbuffered;
86
87impl sealed::Sealed for Cached {}
88impl sealed::Sealed for Unbuffered {}
89
90impl Buffering for Cached {
91    const FLAGS: u32 = 0;
92    const WARRANTY: WarrantyClass = WarrantyClass::BestEffort;
93    const CAPABILITIES: Capabilities = Capabilities::NONE;
94    const LOCAL_DEVICE_ONLY: bool = false;
95
96    fn geometry() -> Geometry {
97        Geometry::UNALIGNED
98    }
99
100    fn sector_geometry(sector_size: u32) -> Result<Geometry, GeometryError> {
101        Geometry::new(sector_size, sector_size, 1)
102    }
103}
104
105impl Unbuffered {
106    /// The sector size an unbuffered file is given until [`OpenedFile::with_sector_size`] says
107    /// otherwise: the largest in general use, which every smaller one divides, so no device
108    /// refuses it. A caller whose device allows less can say so and be offered less alignment to
109    /// meet.
110    pub const DEFAULT_SECTOR_SIZE: u32 = 4096;
111}
112
113impl Buffering for Unbuffered {
114    const FLAGS: u32 = FILE_FLAG_NO_BUFFERING;
115    const WARRANTY: WarrantyClass = WarrantyClass::SharedDevice;
116    const CAPABILITIES: Capabilities = Capabilities::NONE.with_write_through();
117    const LOCAL_DEVICE_ONLY: bool = true;
118
119    fn geometry() -> Geometry {
120        Geometry::uniform(Self::DEFAULT_SECTOR_SIZE).expect("4096 is a power of two")
121    }
122
123    fn sector_geometry(sector_size: u32) -> Result<Geometry, GeometryError> {
124        Geometry::uniform(sector_size)
125    }
126}
127
128/// What to open: access and creation, as `std::fs::OpenOptions` names them. Only these, because
129/// the open mode is what an [`OpenedFile`] vouches for, and a caller's own flags could change it.
130/// A file is shared for reading, writing and deletion, as std opens one.
131#[derive(Clone, Copy, Debug, Default)]
132#[must_use]
133pub struct FileOptions {
134    read: bool,
135    write: bool,
136    create: bool,
137    create_new: bool,
138    truncate: bool,
139}
140
141impl FileOptions {
142    /// Nothing requested: set at least read or write before opening.
143    pub fn new() -> Self {
144        Self::default()
145    }
146
147    /// Open for reading.
148    pub fn read(mut self, read: bool) -> Self {
149        self.read = read;
150        self
151    }
152
153    /// Open for writing.
154    pub fn write(mut self, write: bool) -> Self {
155        self.write = write;
156        self
157    }
158
159    /// Create the file if it does not exist.
160    pub fn create(mut self, create: bool) -> Self {
161        self.create = create;
162        self
163    }
164
165    /// Create the file, and fail if it exists.
166    pub fn create_new(mut self, create_new: bool) -> Self {
167        self.create_new = create_new;
168        self
169    }
170
171    /// Truncate the file to nothing on opening.
172    pub fn truncate(mut self, truncate: bool) -> Self {
173        self.truncate = truncate;
174        self
175    }
176
177    /// What this asks of `CreateFileW`, with `flags` added to the flags it implies, or the same
178    /// refusal `std::fs::OpenOptions` gave for a combination that cannot mean anything: no
179    /// access, or creating or truncating without write access.
180    fn parameters(self, flags: u32) -> io::Result<open_request::Parameters> {
181        let access = match (self.read, self.write) {
182            (true, false) => GENERIC_READ,
183            (false, true) => GENERIC_WRITE,
184            (true, true) => GENERIC_READ | GENERIC_WRITE,
185            (false, false) => {
186                return Err(invalid_input(if self.creates_or_truncates() {
187                    "creating or truncating a file requires write access"
188                } else {
189                    "must specify at least one of read or write access"
190                }));
191            }
192        };
193        if !self.write && self.creates_or_truncates() {
194            return Err(invalid_input(
195                "creating or truncating a file requires write access",
196            ));
197        }
198        // `CREATE_ALWAYS` is not used: a created-and-truncated file is opened with `OPEN_ALWAYS`
199        // and emptied once open, as std does.
200        let (disposition, truncate_after) = match (self.create, self.truncate, self.create_new) {
201            (false, false, false) => (OPEN_EXISTING, false),
202            (true, false, false) => (OPEN_ALWAYS, false),
203            (false, true, false) => (TRUNCATE_EXISTING, false),
204            (true, true, false) => (OPEN_ALWAYS, true),
205            (_, _, true) => (CREATE_NEW, false),
206        };
207        // A new file is never created through a link that is already there.
208        let reparse = if self.create_new {
209            FILE_FLAG_OPEN_REPARSE_POINT
210        } else {
211            0
212        };
213        Ok(open_request::Parameters {
214            access,
215            disposition,
216            flags: flags | reparse,
217            truncate_after,
218        })
219    }
220
221    fn creates_or_truncates(self) -> bool {
222        self.create || self.create_new || self.truncate
223    }
224}
225
226fn invalid_input(message: &'static str) -> io::Error {
227    io::Error::new(io::ErrorKind::InvalidInput, message)
228}
229
230/// An unbuffered file was asked for on a device of another machine, which cannot keep the claim
231/// that makes a file unbuffered: what it reports is the remote system's, not the device's.
232#[derive(Clone, Debug, PartialEq, Eq)]
233pub struct RemoteDevice {
234    /// The path the file would have been opened at, resolved through any link or mapped drive.
235    pub location: PathBuf,
236}
237
238impl fmt::Display for RemoteDevice {
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        write!(
241            f,
242            "{} is on a remote device, which cannot be opened unbuffered with the warranty that implies",
243            self.location.display()
244        )
245    }
246}
247
248impl std::error::Error for RemoteDevice {}
249
250/// A file this crate opened, in buffering mode `B`: a [`SharedHandle`] and what is known of it
251/// because of how it was opened. Cloning shares the one handle.
252///
253/// It is what a [`Win32FileTarget`](super::Win32FileTarget) is made from, and the only thing it
254/// can be made from; the module documentation says why.
255///
256/// A consumer that needs the stronger warranty says so in its signature, and a cached file cannot
257/// be passed:
258///
259/// ```compile_fail,E0308
260/// use win_durable_ioring::windows_backend::{Cached, FileOptions, OpenedFile, Unbuffered};
261///
262/// fn needs_an_unbuffered_file(_: OpenedFile<Unbuffered>) {}
263///
264/// let cached = OpenedFile::<Cached>::open("a-file", FileOptions::new().read(true)).unwrap();
265/// needs_an_unbuffered_file(cached);
266/// ```
267#[derive(Clone, Debug)]
268pub struct OpenedFile<B: Buffering = Cached> {
269    handle: SharedHandle,
270    geometry: Geometry,
271    buffering: PhantomData<B>,
272}
273
274impl<B: Buffering> OpenedFile<B> {
275    /// Open `path` as `options` says, in buffering mode `B`.
276    ///
277    /// # Errors
278    ///
279    /// Whatever the open reports, and [`RemoteDevice`] (as an [`io::ErrorKind::Unsupported`] error)
280    /// when `B` can claim its warranty only on a local device and `path` is on a remote one. That
281    /// is decided from the path's final location, which resolves links and mapped drives, before
282    /// anything is created; the file can still be replaced by a link in the time between.
283    pub fn open(path: impl AsRef<Path>, options: FileOptions) -> io::Result<Self> {
284        let path = path.as_ref();
285        if B::LOCAL_DEVICE_ONLY {
286            let location = final_location(path)?;
287            if names_a_remote_device(&location) {
288                return Err(io::Error::new(
289                    io::ErrorKind::Unsupported,
290                    RemoteDevice { location },
291                ));
292            }
293        }
294        let handle = open_request::open(path, options.parameters(B::FLAGS)?)?;
295        Ok(Self {
296            handle: SharedHandle::from(handle),
297            geometry: B::geometry(),
298            buffering: PhantomData,
299        })
300    }
301
302    /// The file's handle, owned and shared.
303    #[must_use]
304    pub fn handle(&self) -> &SharedHandle {
305        &self.handle
306    }
307
308    /// The alignment its operations need.
309    #[must_use]
310    pub fn geometry(&self) -> Geometry {
311        self.geometry
312    }
313}
314
315impl OpenedFile<Unbuffered> {
316    /// State the device's sector size, for a device whose sectors are smaller than
317    /// [`Unbuffered::DEFAULT_SECTOR_SIZE`]. A size that is too small for the device is refused by
318    /// the device on the first transfer that needs more, as an error on that operation's
319    /// completion, never as silent damage.
320    ///
321    /// # Errors
322    ///
323    /// [`GeometryError`] if `sector_size` is not a power of two.
324    pub fn with_sector_size(mut self, sector_size: u32) -> Result<Self, GeometryError> {
325        self.geometry = Geometry::uniform(sector_size)?;
326        Ok(self)
327    }
328}
329
330impl<B: Buffering> AsHandle for OpenedFile<B> {
331    fn as_handle(&self) -> BorrowedHandle<'_> {
332        self.handle.as_handle()
333    }
334}
335
336impl<B: Buffering> AsRawHandle for OpenedFile<B> {
337    fn as_raw_handle(&self) -> RawHandle {
338        self.handle.as_raw_handle()
339    }
340}
341
342/// Where `path` finally lands: itself if it exists, else the directory it would be created in.
343fn final_location(path: &Path) -> io::Result<PathBuf> {
344    match fs::canonicalize(path) {
345        Err(error) if error.kind() == io::ErrorKind::NotFound => {
346            let parent = path
347                .parent()
348                .filter(|parent| !parent.as_os_str().is_empty())
349                .unwrap_or(Path::new("."));
350            fs::canonicalize(parent)
351        }
352        other => other,
353    }
354}
355
356/// Whether a resolved path names a device of another machine: a UNC share, in either of its
357/// forms. A mapped drive and a link to a share resolve to one, so they are seen as well.
358fn names_a_remote_device(location: &Path) -> bool {
359    matches!(
360        location.components().next(),
361        Some(Component::Prefix(prefix))
362            if matches!(prefix.kind(), Prefix::UNC(..) | Prefix::VerbatimUNC(..))
363    )
364}