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}