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//!
16//! The open reflects `CreateFileW` and not `std::fs::OpenOptions` (DI-D-64): the path is a
17//! `Wtf16String` handed over as given, the [`Disposition`] is Win32's own, and no option implies
18//! another (a new file does not set the reparse flag by itself). It is a build,
19//! [`OpenedFile::request`], and a perform, [`OpenRequest::perform`], with
20//! [`OpenedFile::open`] the two run together.
21
22use std::fmt;
23use std::io;
24use std::marker::PhantomData;
25use std::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle, RawHandle};
26use std::path::{Component, Path, PathBuf, Prefix};
27
28use win_authorization_sys::{SecurityAttributes, SecurityDescriptor};
29use win_shared_os_owned_handle::SharedHandle;
30use windows_namespace_request_sys::open::OpenFile;
31use windows_namespace_request_sys::{FinalPathError, QueryFinalPath};
32use windows_sys::Win32::Foundation::ERROR_SHARING_VIOLATION;
33use windows_sys::Win32::Storage::FileSystem::{
34 FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_OPEN_NO_RECALL, FILE_FLAG_OPEN_REPARSE_POINT,
35 FILE_FLAG_OVERLAPPED, FILE_FLAG_POSIX_SEMANTICS, FILE_FLAG_RANDOM_ACCESS,
36 FILE_FLAG_SEQUENTIAL_SCAN, FILE_READ_DATA, FILE_WRITE_DATA,
37};
38use wtf_string::{Wtf16Str, Wtf16String};
39
40use durable_ioring::{
41 Access, Capabilities, DataAccess, Geometry, GeometryError, NoData, ReadWrite, WarrantyClass,
42};
43
44use super::disposition::Disposition;
45use super::file_attributes::FileAttributes;
46use super::file_id::FileIdentifier;
47use super::file_rights::FileRights;
48use super::share_mode::ShareMode;
49
50mod by_id;
51mod open_request;
52mod reopen;
53
54pub use by_id::{ByIdOptions, ByIdRequest, IdentityMismatch};
55pub use reopen::{ReopenOptions, ReopenRequest};
56
57#[cfg(test)]
58mod creation_tests;
59#[cfg(test)]
60mod flag_tests;
61#[cfg(test)]
62mod request_tests;
63#[cfg(test)]
64mod share_tests;
65#[cfg(test)]
66mod tests;
67
68/// `FILE_FLAG_NO_BUFFERING`, as `CreateFileW` documents it: the file is opened without the system
69/// cache, so every transfer needs the device's alignment.
70const FILE_FLAG_NO_BUFFERING: u32 = 0x2000_0000;
71
72mod sealed {
73 pub trait Sealed {}
74}
75
76/// How a file is buffered, which decides what a target made from it can claim. Sealed: there are
77/// two modes, [`Cached`] and [`Unbuffered`], and a warranty is a statement about each of them that
78/// this crate makes.
79pub trait Buffering: sealed::Sealed + Copy + fmt::Debug + Send + Sync + 'static {
80 /// The `CreateFileW` flags this mode opens with.
81 const FLAGS: u32;
82
83 /// The warranty class a file in this mode can claim.
84 const WARRANTY: WarrantyClass;
85
86 /// What a file in this mode offers beyond the contract's obligations.
87 const CAPABILITIES: Capabilities;
88
89 /// Whether the claim in [`WARRANTY`](Self::WARRANTY) holds only on a device of this machine, so
90 /// that a file on a remote one is refused rather than given a class it cannot keep.
91 const LOCAL_DEVICE_ONLY: bool;
92
93 /// The alignment a file in this mode starts with.
94 fn geometry() -> Geometry;
95
96 /// The alignment a raw volume or disk in this mode needs, addressed in sectors of `sector_size`
97 /// bytes. Its transfers are whole sectors in either mode, so offsets and lengths are aligned to
98 /// one; its buffers only when it is unbuffered, as the cache is what lets a cached transfer
99 /// start anywhere.
100 ///
101 /// # Errors
102 ///
103 /// [`GeometryError`] if `sector_size` is not a power of two.
104 fn sector_geometry(sector_size: u32) -> Result<Geometry, GeometryError>;
105}
106
107/// A file opened for cached I/O: the system cache holds its writes, and its write-back can fail
108/// without telling the writer, so it claims [`WarrantyClass::BestEffort`] and needs no alignment.
109#[derive(Clone, Copy, Debug)]
110pub struct Cached;
111
112/// A file opened with no intermediate buffering: its transfers reach the device, so it can claim
113/// [`WarrantyClass::SharedDevice`], and need the device's sector alignment. Per-write
114/// write-through is honoured only on such a file, which the kernel refuses on a cached one
115/// (error 509, measured 2026-10-07 and kept as a test), so it offers that capability.
116#[derive(Clone, Copy, Debug)]
117pub struct Unbuffered;
118
119impl sealed::Sealed for Cached {}
120impl sealed::Sealed for Unbuffered {}
121
122impl Buffering for Cached {
123 const FLAGS: u32 = 0;
124 const WARRANTY: WarrantyClass = WarrantyClass::BestEffort;
125 const CAPABILITIES: Capabilities = Capabilities::NONE;
126 const LOCAL_DEVICE_ONLY: bool = false;
127
128 fn geometry() -> Geometry {
129 Geometry::UNALIGNED
130 }
131
132 fn sector_geometry(sector_size: u32) -> Result<Geometry, GeometryError> {
133 Geometry::new(sector_size, sector_size, 1)
134 }
135}
136
137impl Unbuffered {
138 /// The sector size an unbuffered file is given until [`OpenedFile::with_sector_size`] says
139 /// otherwise: the largest in general use, which every smaller one divides, so no device
140 /// refuses it. A caller whose device allows less can say so and be offered less alignment to
141 /// meet.
142 pub const DEFAULT_SECTOR_SIZE: u32 = 4096;
143}
144
145impl Buffering for Unbuffered {
146 const FLAGS: u32 = FILE_FLAG_NO_BUFFERING;
147 const WARRANTY: WarrantyClass = WarrantyClass::SharedDevice;
148 const CAPABILITIES: Capabilities = Capabilities::NONE.with_write_through();
149 const LOCAL_DEVICE_ONLY: bool = true;
150
151 fn geometry() -> Geometry {
152 Geometry::uniform(Self::DEFAULT_SECTOR_SIZE).expect("4096 is a power of two")
153 }
154
155 fn sector_geometry(sector_size: u32) -> Result<Geometry, GeometryError> {
156 Geometry::uniform(sector_size)
157 }
158}
159
160/// What to open: the data access, the other rights, what other opens may do meanwhile, what the open
161/// does about a file that is there or not, whether a link is opened or followed, and the cache hints.
162/// Only these, because the open mode is what an [`OpenedFile`] vouches for, and a caller's own flags
163/// could change it. A file is shared for reading, writing and deletion, as std opens one, until
164/// [`share`](Self::share) says otherwise.
165///
166/// The data access is the type parameter `A`: it starts as [`NoData`], and [`read`](Self::read) and
167/// [`write`](Self::write) move it, so the handle an open makes carries the access it was asked
168/// for.
169///
170/// ```
171/// use win_durable_ioring::windows_backend::{FileOptions, FileRights, ReadWrite};
172///
173/// let options: FileOptions<ReadWrite> = FileOptions::new()
174/// .read()
175/// .write()
176/// .rights(FileRights::WRITE_ATTRIBUTES);
177/// ```
178#[derive(Clone, Copy, Debug)]
179#[must_use]
180pub struct FileOptions<A: DataAccess = NoData> {
181 access: PhantomData<A>,
182 rights: FileRights,
183 share: ShareMode,
184 disposition: Disposition,
185 open_reparse_point: bool,
186 attributes: FileAttributes,
187 delete_on_close: bool,
188 open_no_recall: bool,
189 posix_semantics: bool,
190 overlapped: bool,
191 random_access: bool,
192 sequential_scan: bool,
193}
194
195impl Default for FileOptions<NoData> {
196 fn default() -> Self {
197 Self::new()
198 }
199}
200
201impl FileOptions<NoData> {
202 /// Nothing requested: no data access and no other right. Ask to [`read`](Self::read) and
203 /// [`write`](Self::write) before opening a file that is to become a target.
204 pub fn new() -> Self {
205 Self {
206 access: PhantomData,
207 rights: FileRights::NONE,
208 share: ShareMode::ALL,
209 disposition: Disposition::OpenExisting,
210 open_reparse_point: false,
211 attributes: FileAttributes::NONE,
212 delete_on_close: false,
213 open_no_recall: false,
214 posix_semantics: false,
215 overlapped: false,
216 random_access: false,
217 sequential_scan: false,
218 }
219 }
220}
221
222impl<A: DataAccess> FileOptions<A> {
223 /// Ask for the right to read the data (`FILE_READ_DATA`). The options carry the access this adds
224 /// to what they already had: [`NoData`] becomes [`ReadOnly`](super::ReadOnly) and
225 /// [`WriteOnly`](super::WriteOnly) becomes [`ReadWrite`].
226 pub fn read(self) -> FileOptions<A::WithRead> {
227 self.with_access()
228 }
229
230 /// Ask for the right to write the data (`FILE_WRITE_DATA`), as [`read`](Self::read) asks to read.
231 pub fn write(self) -> FileOptions<A::WithWrite> {
232 self.with_access()
233 }
234
235 /// The rights to open with besides the data access (`dwDesiredAccess`): the attribute, delete and
236 /// security rights, which gate calls made on the handle this crate lends and not anything it
237 /// does itself. What the file's security allows is the kernel's to say, and its refusal reaches
238 /// the caller as its own error.
239 ///
240 /// Calling this again replaces the rights; join them with `|` to ask for several.
241 pub fn rights(mut self, rights: FileRights) -> Self {
242 self.rights = rights;
243 self
244 }
245
246 /// What other opens of the file may request while this handle is open (`dwShareMode`). The
247 /// default is [`ShareMode::ALL`]; [`ShareMode::NONE`] is an exclusive open, which a database that
248 /// owns its file wants.
249 ///
250 /// An open the sharing of either side does not allow fails with a [`SharingViolation`], whichever
251 /// of the two opens came first. Calling this again replaces the mode.
252 pub fn share(mut self, share: ShareMode) -> Self {
253 self.share = share;
254 self
255 }
256
257 fn with_access<A2: DataAccess>(self) -> FileOptions<A2> {
258 FileOptions {
259 access: PhantomData,
260 rights: self.rights,
261 share: self.share,
262 disposition: self.disposition,
263 open_reparse_point: self.open_reparse_point,
264 attributes: self.attributes,
265 delete_on_close: self.delete_on_close,
266 open_no_recall: self.open_no_recall,
267 posix_semantics: self.posix_semantics,
268 overlapped: self.overlapped,
269 random_access: self.random_access,
270 sequential_scan: self.sequential_scan,
271 }
272 }
273
274 /// What the open does about a file that is there and one that is not (`dwCreationDisposition`).
275 /// The default is [`Disposition::OpenExisting`]. Calling this again replaces the disposition.
276 ///
277 /// No disposition changes the contents of a file that exists, so none is refused for want of the
278 /// right to write. A caller that wants an existing file emptied does it through the handle this
279 /// crate lends, with `File::set_len(0)`, which needs the write right and is not atomic with the
280 /// open.
281 pub fn disposition(mut self, disposition: Disposition) -> Self {
282 self.disposition = disposition;
283 self
284 }
285
286 /// Open a link itself and not what it points to (`FILE_FLAG_OPEN_REPARSE_POINT`). Without it a
287 /// link is followed, including by [`Disposition::CreateNew`], which then creates the file the
288 /// link names. With it a link that is already there is what exists, so `CreateNew` fails and
289 /// `OpenAlways` opens the link. It has no effect on a path that is not a reparse point.
290 pub fn open_reparse_point(mut self, open_reparse_point: bool) -> Self {
291 self.open_reparse_point = open_reparse_point;
292 self
293 }
294
295 /// The attributes a file the open creates is given (`FILE_ATTRIBUTE_*` in `dwFlagsAndAttributes`).
296 /// The default is none, which is not `FILE_ATTRIBUTE_NORMAL`: the empty set passes no attribute at
297 /// all, and a created file has the attributes the file system gives it, `ARCHIVE` among them
298 /// (measured: the same as an open that passes `NORMAL`). Calling this again replaces the set.
299 ///
300 /// They are for a file that is created, and what an open of a file that exists does with them is
301 /// Win32's: the `CreateFileW` page says they are generally combined with the file's own or
302 /// ignored, and nothing here changes an existing file's attributes (measured: the open leaves them
303 /// as they were).
304 pub fn attributes(mut self, attributes: FileAttributes) -> Self {
305 self.attributes = attributes;
306 self
307 }
308
309 /// Delete the file when its last handle is closed (`FILE_FLAG_DELETE_ON_CLOSE`). A durability
310 /// report is then about data that does not outlive the handles, which is the caller's choice to
311 /// make.
312 ///
313 /// The file goes when the last handle to it, however that was opened, is closed. Measured on
314 /// Windows 11 build 26300: the open succeeds without the right to delete among the
315 /// [`rights`](Self::rights) (the `CreateFileW` pages do not say it is needed; `ZwCreateFile`'s
316 /// does, for its own flag, and that is not the call made here), and while the
317 /// handle is open a later open has to share deletion ([`ShareMode::DELETE`]) or it fails with a
318 /// [`SharingViolation`]. Nothing is added or checked here; which combinations are refused ahead
319 /// of the open is `DI-14.4`'s.
320 pub fn delete_on_close(mut self, delete_on_close: bool) -> Self {
321 self.delete_on_close = delete_on_close;
322 self
323 }
324
325 /// Ask that the file's data not be brought back from remote storage to be read
326 /// (`FILE_FLAG_OPEN_NO_RECALL`). The `CreateFileW` page says the flag is for remote storage
327 /// systems; it is passed on and nothing about a target depends on it. What it does to a file on
328 /// a local disk was not measured.
329 pub fn open_no_recall(mut self, open_no_recall: bool) -> Self {
330 self.open_no_recall = open_no_recall;
331 self
332 }
333
334 /// Access the file by POSIX rules (`FILE_FLAG_POSIX_SEMANTICS`). The `CreateFileW` page says
335 /// that this includes allowing names that differ only in case, and that a file made with the flag
336 /// may not be reachable by applications written for MS-DOS or 16-bit Windows. It is passed on and
337 /// nothing about a target depends on it.
338 pub fn posix_semantics(mut self, posix_semantics: bool) -> Self {
339 self.posix_semantics = posix_semantics;
340 self
341 }
342
343 /// Open the file for overlapped I/O (`FILE_FLAG_OVERLAPPED`). Every operation this crate's targets
344 /// make is addressed by offset, so the file pointer the system then stops keeping is not one they
345 /// read. Measured on Windows 11 build 26300 (the run is in `DI-D-64`), the ring takes such a
346 /// handle and reports the same result for each operation as for one opened without the flag, in
347 /// each buffering mode. What differs is when a completion is there: a handle opened without the
348 /// flag completes during the submit, and an unbuffered one opened with it was not complete then
349 /// for its reads, which then completed in an order that was not always the order queued.
350 ///
351 /// The handle this crate lends is the same one, so I/O a caller makes on it directly without an
352 /// offset, with a `std::fs::File` for one, is I/O on an overlapped handle, where the
353 /// `CreateFileW` page says the file pointer is not maintained and operations are not serialised.
354 pub fn overlapped(mut self, overlapped: bool) -> Self {
355 self.overlapped = overlapped;
356 self
357 }
358
359 /// Hint that the file will be accessed at random (`FILE_FLAG_RANDOM_ACCESS`), which the system
360 /// may use to tune its caching.
361 ///
362 /// It is passed to an unbuffered file as well as a cached one, and is never refused. The
363 /// `CreateFileW` documentation says the flag "has no effect if the file system does not support
364 /// cached I/O and `FILE_FLAG_NO_BUFFERING`", which can be read two ways, so what it does to an
365 /// unbuffered file is the kernel's to say. Measured on Windows 11 build 26300: every combination
366 /// of the two hints, cached and unbuffered, opens and carries a sector-aligned write and read,
367 /// and an unbuffered one also runs through the ring. Whether a hint changes the system's caching
368 /// was not measured, and nothing about the warranty class, the alignment or write-through
369 /// depends on it. Asking for this and [`sequential_scan`](Self::sequential_scan) together is
370 /// the documentation's "self-defeating", and is passed on all the same.
371 pub fn random_access(mut self, random_access: bool) -> Self {
372 self.random_access = random_access;
373 self
374 }
375
376 /// Hint that the file will be read sequentially from beginning to end
377 /// (`FILE_FLAG_SEQUENTIAL_SCAN`), which the system may use to tune its caching. The
378 /// documentation says it should not be used if reverse scans will be.
379 ///
380 /// Passed through and documented as [`random_access`](Self::random_access) is.
381 pub fn sequential_scan(mut self, sequential_scan: bool) -> Self {
382 self.sequential_scan = sequential_scan;
383 self
384 }
385
386 /// What this asks of `CreateFileW`, with `flags` added to the flags it implies. Every
387 /// combination of the options is a request, so nothing here is refused.
388 fn parameters(self, flags: u32) -> open_request::Parameters {
389 let reparse = if self.open_reparse_point {
390 FILE_FLAG_OPEN_REPARSE_POINT
391 } else {
392 0
393 };
394 let mut named = 0;
395 if self.delete_on_close {
396 named |= FILE_FLAG_DELETE_ON_CLOSE;
397 }
398 if self.open_no_recall {
399 named |= FILE_FLAG_OPEN_NO_RECALL;
400 }
401 if self.posix_semantics {
402 named |= FILE_FLAG_POSIX_SEMANTICS;
403 }
404 if self.overlapped {
405 named |= FILE_FLAG_OVERLAPPED;
406 }
407 let mut hints = 0;
408 if self.random_access {
409 hints |= FILE_FLAG_RANDOM_ACCESS;
410 }
411 if self.sequential_scan {
412 hints |= FILE_FLAG_SEQUENTIAL_SCAN;
413 }
414 open_request::Parameters {
415 access: data_rights::<A>() | self.rights.bits(),
416 disposition: self.disposition.bits(),
417 flags: flags | reparse | named | hints | self.attributes.bits(),
418 share: self.share.bits(),
419 }
420 }
421}
422
423/// What `A` asks `CreateFileW` for: `FILE_READ_DATA` as it reads and `FILE_WRITE_DATA` as it writes,
424/// and no other right. The append bit is never among them, so a handle that appends and cannot write
425/// at an offset cannot be asked for. The markers are the core's (DI-D-66); what they ask of Win32 is
426/// stated here.
427pub(super) const fn data_rights<A: DataAccess>() -> u32 {
428 (if A::READ { FILE_READ_DATA } else { 0 }) | (if A::WRITE { FILE_WRITE_DATA } else { 0 })
429}
430
431/// An unbuffered file was asked for on a device of another machine, which cannot keep the claim
432/// that makes a file unbuffered: what it reports is the remote system's, not the device's.
433#[derive(Clone, Debug, PartialEq, Eq)]
434pub struct RemoteDevice {
435 /// The path the file would have been opened at, resolved through any link or mapped drive.
436 pub location: PathBuf,
437}
438
439impl fmt::Display for RemoteDevice {
440 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
441 write!(
442 f,
443 "{} is on a remote device, which cannot be opened unbuffered with the warranty that implies",
444 self.location.display()
445 )
446 }
447}
448
449impl std::error::Error for RemoteDevice {}
450
451/// An unbuffered file was asked for on a handle whose location could not be read: it has no final
452/// path (`NUL` has none), so it is not known to be a file on a local volume, which is what the
453/// warranty of an unbuffered file rests on.
454///
455/// This is this crate's own narrowing, not a condition `CreateFileW` documents: the open itself
456/// succeeded, and the handle is closed. A cached open of the same name is not refused. The call's
457/// own error is the [`source`](std::error::Error::source), kept as the call reported it, and is not
458/// a statement of what the handle is.
459#[derive(Debug)]
460pub struct UnknownLocation {
461 /// What reading the handle's final path reported.
462 pub cause: io::Error,
463}
464
465impl fmt::Display for UnknownLocation {
466 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
467 write!(
468 f,
469 "the handle's final path could not be read, so it is not known to be a file on a local volume, which an unbuffered file needs: {}",
470 self.cause
471 )
472 }
473}
474
475impl std::error::Error for UnknownLocation {
476 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
477 Some(&self.cause)
478 }
479}
480
481/// An open failed because of how the file is already open: either this open asked for an access that
482/// an open already holding the file does not share, or it did not share an access that open asked for.
483/// `CreateFileW` fails both ways with `ERROR_SHARING_VIOLATION` and says nothing of which, and no
484/// documented call reports the other open's access or sharing, so this carries what *this* open asked
485/// for and nothing about the other.
486///
487/// It is the cause of an [`io::ErrorKind::ResourceBusy`] error from [`OpenedFile::open`], reached with
488/// [`io::Error::get_ref`] and a downcast, and of the same error from [`OpenedFile::open_by_id`]. The
489/// kernel's own error, which carries the Win32 code, is its [`source`](std::error::Error::source).
490#[derive(Debug)]
491pub struct SharingViolation {
492 /// What the open named the file by.
493 pub by: OpenedBy,
494 /// The data access the open asked for, or `None` for one that asked for no data right.
495 pub data: Option<Access>,
496 /// The rights the open asked for besides the data access.
497 pub rights: FileRights,
498 /// What this open allowed other opens to request.
499 pub share: ShareMode,
500 os_error: io::Error,
501}
502
503impl fmt::Display for SharingViolation {
504 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
505 write!(
506 f,
507 "{} is open in a way that conflicts with this open (data access {}, {:?}, sharing {:?}): \
508 the other open does not share what this one asked for, or this one does not share what \
509 the other holds",
510 self.by,
511 self.data
512 .map_or_else(|| "none".to_owned(), |data| data.to_string()),
513 self.rights,
514 self.share,
515 )
516 }
517}
518
519impl std::error::Error for SharingViolation {
520 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
521 Some(&self.os_error)
522 }
523}
524
525/// What an open named the file by: the path it was given, the identifier it opened on the hint's
526/// volume, or, for a reopen, the handle it was given.
527#[derive(Clone, Debug, PartialEq, Eq)]
528pub enum OpenedBy {
529 /// A path, as it was given.
530 Path(PathBuf),
531 /// An identifier, by [`OpenedFile::open_by_id`].
532 Identifier(FileIdentifier),
533 /// What the original handle of a reopen names, by [`OpenedFile::reopen`]. The handle is the
534 /// caller's own, so it is not carried.
535 Reopen,
536}
537
538impl fmt::Display for OpenedBy {
539 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
540 match self {
541 Self::Path(path) => write!(f, "{}", path.display()),
542 Self::Identifier(identifier) => write!(f, "the file identified by {identifier:?}"),
543 Self::Reopen => write!(f, "the file the reopened handle names"),
544 }
545 }
546}
547
548/// The data access `A` asks for, as a value, or `None` for one that asks for no data right.
549pub(super) fn data_access<A: DataAccess>() -> Option<Access> {
550 match (A::READ, A::WRITE) {
551 (false, false) => None,
552 (true, false) => Some(Access::ReadOnly),
553 (false, true) => Some(Access::WriteOnly),
554 (true, true) => Some(Access::ReadWrite),
555 }
556}
557
558/// `error`, or the [`SharingViolation`] it is if the kernel said so.
559pub(super) fn refine_sharing(
560 error: io::Error,
561 path: &Wtf16Str,
562 data: Option<Access>,
563 rights: FileRights,
564 share: ShareMode,
565) -> io::Error {
566 refine_sharing_by(
567 error,
568 || OpenedBy::Path(PathBuf::from(path.to_os_string())),
569 data,
570 rights,
571 share,
572 )
573}
574
575/// `error`, or the [`SharingViolation`] it is if the kernel said so, naming the file as `by` does.
576fn refine_sharing_by(
577 error: io::Error,
578 by: impl FnOnce() -> OpenedBy,
579 data: Option<Access>,
580 rights: FileRights,
581 share: ShareMode,
582) -> io::Error {
583 if error.raw_os_error() != Some(ERROR_SHARING_VIOLATION.cast_signed()) {
584 return error;
585 }
586 io::Error::new(
587 io::ErrorKind::ResourceBusy,
588 SharingViolation {
589 by: by(),
590 data,
591 rights,
592 share,
593 os_error: error,
594 },
595 )
596}
597
598/// A file this crate opened, in buffering mode `B` and with data access `A`: a [`SharedHandle`] and
599/// what is known of it because of how it was opened. Cloning shares the one handle.
600///
601/// It is what a [`Win32FileTarget`](super::Win32FileTarget) is made from, and the only thing it
602/// can be made from; the module documentation says why. A target needs a data right, so a file
603/// opened with [`NoData`] cannot be made into one.
604///
605/// A consumer that needs the stronger warranty says so in its signature, and a cached file cannot
606/// be passed:
607///
608/// ```compile_fail,E0308
609/// use win_durable_ioring::Wtf16String;
610/// use win_durable_ioring::windows_backend::{Cached, FileOptions, OpenedFile, Unbuffered};
611///
612/// fn needs_an_unbuffered_file(_: OpenedFile<Unbuffered>) {}
613///
614/// let options = FileOptions::new().read().write();
615/// let name = Wtf16String::from_os_str("a-file".as_ref());
616/// let cached = OpenedFile::<Cached>::open(name, options).unwrap();
617/// needs_an_unbuffered_file(cached);
618/// ```
619///
620/// Nor can one opened for reading only be given where reading and writing are asked for:
621///
622/// ```compile_fail,E0308
623/// use win_durable_ioring::Wtf16String;
624/// use win_durable_ioring::windows_backend::{Cached, FileOptions, OpenedFile, ReadWrite};
625///
626/// fn needs_both(_: OpenedFile<Cached, ReadWrite>) {}
627///
628/// let name = Wtf16String::from_os_str("a-file".as_ref());
629/// let read_only = OpenedFile::<Cached, _>::open(name, FileOptions::new().read()).unwrap();
630/// needs_both(read_only);
631/// ```
632///
633/// The data access is a type parameter after the buffering mode, and a parameter written in a path
634/// takes its default, [`ReadWrite`], when it is left off, so a file opened with other options names it
635/// or leaves it to be inferred with `_`: `OpenedFile::<Cached, _>::open(path, options)`.
636#[derive(Clone, Debug)]
637pub struct OpenedFile<B: Buffering = Cached, A: DataAccess = ReadWrite> {
638 handle: SharedHandle,
639 geometry: Geometry,
640 mode: PhantomData<(B, A)>,
641}
642
643impl<B: Buffering, A: DataAccess> OpenedFile<B, A> {
644 /// Open `path` as `options` says, in buffering mode `B`: [`request`](Self::request) and then
645 /// [`perform`](OpenRequest::perform), one after the other on this thread. The handle has the data
646 /// access the options were given, which is its type `A`.
647 ///
648 /// **This blocks for as long as `CreateFileW` does**, which on a network path or an opportunistic
649 /// lock break can be a long time. A caller that cannot block builds the request here and performs
650 /// it on a thread it chooses.
651 ///
652 /// **Where the name leads is the caller's to know.** A name can be redirected to a device by a
653 /// reparse point in an intermediate directory, which `FILE_FLAG_OPEN_REPARSE_POINT` does not stop
654 /// (it applies to the final component), or by an object-manager link such as a drive letter, which
655 /// is resolved before a file system sees the name, so no reparse flag applies to it. An open of
656 /// a serial port's name was observed not to return. This crate does not inspect the name, refuse it,
657 /// bound the open or cancel it: the open is `CreateFileW`'s, and `CancelSynchronousIo` is documented
658 /// as a use and not as a guarantee, and targets a thread and not a call. The design notes' `DI-D-67`
659 /// has the sources and the measurement.
660 ///
661 /// # Errors
662 ///
663 /// Those of [`request`](Self::request) and of [`perform`](OpenRequest::perform).
664 pub fn open(path: Wtf16String, options: FileOptions<A>) -> io::Result<Self> {
665 Self::request(path, options)?.perform()
666 }
667
668 /// The request to open `path` as `options` says, in buffering mode `B`, built here and made
669 /// wherever the caller chooses with [`perform`](OpenRequest::perform). Nothing is opened, and
670 /// nothing blocks.
671 ///
672 /// The path is handed to Win32 as it was given: this crate does not lengthen it, shorten it or
673 /// refuse it, so a path longer than `MAX_PATH` without a verbatim prefix, and an empty one, are
674 /// what `CreateFileW` makes of them. A relative path is fixed to the current directory as it is
675 /// when this is called, so the request names the same file wherever and whenever it is performed.
676 /// Convert a `Path` with [`Wtf16String::from_os_str`], which loses nothing.
677 ///
678 /// # Errors
679 ///
680 /// [`io::ErrorKind::InvalidInput`] if `path` has an interior NUL, and the error of
681 /// `GetCurrentDirectoryW` if the current directory had to be read and could not be, both before
682 /// anything is touched.
683 pub fn request(path: Wtf16String, options: FileOptions<A>) -> io::Result<OpenRequest<B, A>> {
684 let (rights, share) = (options.rights, options.share);
685 let request = open_request::request(path, options.parameters(B::FLAGS))?;
686 Ok(OpenRequest {
687 request,
688 rights,
689 share,
690 mode: PhantomData,
691 })
692 }
693
694 /// The file's handle, owned and shared.
695 #[must_use]
696 pub fn handle(&self) -> &SharedHandle {
697 &self.handle
698 }
699
700 /// The alignment its operations need.
701 #[must_use]
702 pub fn geometry(&self) -> Geometry {
703 self.geometry
704 }
705}
706
707/// The request to open one file, built by [`OpenedFile::request`] and made by
708/// [`perform`](Self::perform): the path, fixed to the current directory if it was relative, and the
709/// options, typed by the buffering mode and the data access of the [`OpenedFile`] it opens.
710///
711/// It is `Send`, so it can be built on the thread that has the path and the current directory and
712/// performed on one that can afford to block, and it can be performed more than once, each time
713/// opening the file again.
714#[derive(Debug)]
715#[must_use = "a request that is not performed opens nothing"]
716pub struct OpenRequest<B: Buffering = Cached, A: DataAccess = ReadWrite> {
717 request: OpenFile,
718 rights: FileRights,
719 share: ShareMode,
720 mode: PhantomData<fn() -> (B, A)>,
721}
722
723impl<B: Buffering, A: DataAccess> OpenRequest<B, A> {
724 /// Give the file this open creates the security descriptor, built from SDDL with
725 /// [`SecurityDescriptor::from_sddl`]. The request owns it and frees it, and a caller never holds
726 /// a pointer to it. Calling this again replaces the descriptor.
727 ///
728 /// It is the request's and not a [`FileOptions`] field because a descriptor owns memory and
729 /// cannot be copied, and the options are a plain `Copy` description that is passed to several
730 /// opens. Handles are never inheritable: the attributes this passes to `CreateFileW` say so
731 /// (`bInheritHandle` is false), and a request given no descriptor passes none, which the
732 /// `CreateFileW` page documents as a handle that cannot be inherited.
733 ///
734 /// The `CreateFileW` page says the descriptor is for a file that is created and is ignored when
735 /// the file exists (measured: an open of an existing file with a descriptor that denies reading
736 /// leaves the file as it was). This crate does not apply the descriptor itself; a created
737 /// file's descriptor reads back as the one given, through `win-authorization-sys`'s
738 /// `get_security_info`, for a file opened with [`FileRights::READ_CONTROL`].
739 pub fn with_security_descriptor(self, descriptor: SecurityDescriptor) -> Self {
740 Self {
741 request: self
742 .request
743 .with_security(Some(SecurityAttributes::new(Some(descriptor), false))),
744 ..self
745 }
746 }
747
748 /// Open the file, on the calling thread. **This blocks for as long as `CreateFileW` does**, and
749 /// where the name leads is the caller's to know, as [`OpenedFile::open`] says.
750 ///
751 /// # Errors
752 ///
753 /// [`RemoteDevice`] (as an [`io::ErrorKind::Unsupported`] error) when `B` can claim its warranty
754 /// only on a local device and the opened handle is on a remote one. That is decided from the
755 /// handle's final path, which `GetFinalPathNameByHandleW` resolves through links and mapped
756 /// drives, so there is no window between the check and the open in which a link can be
757 /// swapped. It is made after the open, so a file created before the refusal stays created and
758 /// the open has already reached the network. [`UnknownLocation`] (also
759 /// [`io::ErrorKind::Unsupported`]) when the handle's final path cannot be read, since it is then
760 /// not known to be a file on a local volume; the call's own error is its source. That is this
761 /// crate's narrowing and not a condition of `CreateFileW`, and only an unbuffered open is subject
762 /// to it: a cached open of a device is not refused, and what a name leads to is the caller's
763 /// responsibility (DI-D-67). Otherwise whatever the open reports, among them [`SharingViolation`] (as an
764 /// [`io::ErrorKind::ResourceBusy`] error) when the file is open in a way that conflicts with
765 /// this open's access or sharing.
766 pub fn perform(&self) -> io::Result<OpenedFile<B, A>> {
767 let handle = open_request::perform(&self.request).map_err(|error| {
768 refine_sharing(
769 error,
770 self.request.path(),
771 data_access::<A>(),
772 self.rights,
773 self.share,
774 )
775 })?;
776 OpenedFile::admit(SharedHandle::from(handle))
777 }
778}
779
780impl<B: Buffering, A: DataAccess> OpenedFile<B, A> {
781 /// The checks every open makes on the handle it opened, whether by path or by identifier, and
782 /// the file it is if they pass (DI-14.5): when `B` claims its warranty only on a local device, the
783 /// handle's final path must be readable and must not name a device of another machine.
784 fn admit(handle: SharedHandle) -> io::Result<Self> {
785 if B::LOCAL_DEVICE_ONLY {
786 let location = final_location(&handle).map_err(|cause| {
787 io::Error::new(io::ErrorKind::Unsupported, UnknownLocation { cause })
788 })?;
789 if names_a_remote_device(&location) {
790 return Err(io::Error::new(
791 io::ErrorKind::Unsupported,
792 RemoteDevice { location },
793 ));
794 }
795 }
796 Ok(OpenedFile {
797 handle,
798 geometry: B::geometry(),
799 mode: PhantomData,
800 })
801 }
802}
803
804impl<A: DataAccess> OpenedFile<Unbuffered, A> {
805 /// State the device's sector size, for a device whose sectors are smaller than
806 /// [`Unbuffered::DEFAULT_SECTOR_SIZE`]. A size that is too small for the device is refused by
807 /// the device on the first transfer that needs more, as an error on that operation's
808 /// completion, never as silent damage.
809 ///
810 /// # Errors
811 ///
812 /// [`GeometryError`] if `sector_size` is not a power of two.
813 pub fn with_sector_size(mut self, sector_size: u32) -> Result<Self, GeometryError> {
814 self.geometry = Geometry::uniform(sector_size)?;
815 Ok(self)
816 }
817}
818
819impl<B: Buffering, A: DataAccess> AsHandle for OpenedFile<B, A> {
820 fn as_handle(&self) -> BorrowedHandle<'_> {
821 self.handle.as_handle()
822 }
823}
824
825impl<B: Buffering, A: DataAccess> AsRawHandle for OpenedFile<B, A> {
826 fn as_raw_handle(&self) -> RawHandle {
827 self.handle.as_raw_handle()
828 }
829}
830
831/// Where an opened handle is: its final path, in `GetFinalPathNameByHandleW`'s default form, which
832/// follows links and mapped drives to the object the handle names. A handle opened on a link itself
833/// (`FILE_FLAG_OPEN_REPARSE_POINT`) names the link, on its own volume.
834///
835/// The request is given the handle shared, not a duplicate of it (DI-16.1).
836fn final_location(handle: &SharedHandle) -> io::Result<PathBuf> {
837 let path = QueryFinalPath::new(handle.clone())
838 .perform()
839 .map_err(|error| match error {
840 FinalPathError::Win32(code) => {
841 io::Error::from_raw_os_error(open_request::raw_code(code.code()))
842 }
843 other => io::Error::other(other),
844 })?;
845 Ok(PathBuf::from(path.to_os_string()))
846}
847
848/// Whether a resolved path names a device of another machine: a UNC share, in either of its
849/// forms. A mapped drive and a link to a share resolve to one, so they are seen as well.
850fn names_a_remote_device(location: &Path) -> bool {
851 matches!(
852 location.components().next(),
853 Some(Component::Prefix(prefix))
854 if matches!(prefix.kind(), Prefix::UNC(..) | Prefix::VerbatimUNC(..))
855 )
856}