win_durable_ioring/windows_backend/opened_file/by_id.rs
1// Copyright (c) 2026 Mike Grier
2//! An open by identifier, through `OpenFileById` (DI-15.2, DI-D-70): the file is named by a
3//! [`FileIdentifier`] on the volume of a hint, and not by a path.
4//!
5//! The options are a type of their own, [`ByIdOptions`], and not [`FileOptions`](super::FileOptions):
6//! the call's page lists no creation disposition, takes no attributes, marks the security parameter
7//! reserved, and lists eight flags, of which `FILE_FLAG_DELETE_ON_CLOSE` and
8//! `FILE_FLAG_POSIX_SEMANTICS` are not two. What a handle this crate opened can claim depends on how
9//! it was opened and not on how it was named, so the file is the same [`OpenedFile`], and the checks
10//! made on the handle are the path open's (DI-14.5).
11//!
12//! The hint is any handle on the volume, whatever opened it: it is never the file opened, so its mode
13//! reaches nothing the result claims (`DI-D-64`'s `hVolumeHint` row).
14
15use std::fmt;
16use std::io;
17use std::marker::PhantomData;
18
19use win_shared_os_owned_handle::SharedHandle;
20use windows_namespace_request_sys::open_by_id::OpenFileByIdentifier;
21use windows_sys::Win32::Storage::FileSystem::{
22 FILE_FLAG_OPEN_NO_RECALL, FILE_FLAG_OPEN_REPARSE_POINT, FILE_FLAG_OVERLAPPED,
23 FILE_FLAG_RANDOM_ACCESS, FILE_FLAG_SEQUENTIAL_SCAN,
24};
25
26use durable_ioring::{DataAccess, NoData, ReadWrite};
27
28use super::{Buffering, Cached, OpenedBy, OpenedFile, data_access, data_rights, refine_sharing_by};
29use crate::windows_backend::file_id::{FileId, FileIdentifier};
30use crate::windows_backend::file_rights::FileRights;
31use crate::windows_backend::share_mode::ShareMode;
32
33#[cfg(test)]
34mod tests;
35
36/// What to open by identifier: the data access, the other rights, what other opens may do meanwhile,
37/// whether a link is opened or followed, and the hints, which are what `OpenFileById` takes. A file is
38/// shared for reading, writing and deletion until [`share`](Self::share) says otherwise, as
39/// [`FileOptions`](super::FileOptions) shares it.
40///
41/// There is no disposition, since the call opens only what exists; no attributes, which its page
42/// says it ignores; no security descriptor, which it marks reserved; and no delete-on-close or POSIX
43/// semantics, which it does not list. `FILE_FLAG_BACKUP_SEMANTICS`, which a directory needs, and
44/// `FILE_FLAG_WRITE_THROUGH` are not in v1, as for an open by path.
45///
46/// ```
47/// use win_durable_ioring::windows_backend::{ByIdOptions, ReadOnly, ShareMode};
48///
49/// let options: ByIdOptions<ReadOnly> = ByIdOptions::new().read().share(ShareMode::READ);
50/// ```
51#[derive(Clone, Copy, Debug)]
52#[must_use]
53pub struct ByIdOptions<A: DataAccess = NoData> {
54 access: PhantomData<A>,
55 rights: FileRights,
56 share: ShareMode,
57 open_reparse_point: bool,
58 open_no_recall: bool,
59 overlapped: bool,
60 random_access: bool,
61 sequential_scan: bool,
62}
63
64impl Default for ByIdOptions<NoData> {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl ByIdOptions<NoData> {
71 /// Nothing requested: no data access and no other right, shared for everything.
72 pub fn new() -> Self {
73 Self {
74 access: PhantomData,
75 rights: FileRights::NONE,
76 share: ShareMode::ALL,
77 open_reparse_point: false,
78 open_no_recall: false,
79 overlapped: false,
80 random_access: false,
81 sequential_scan: false,
82 }
83 }
84}
85
86impl<A: DataAccess> ByIdOptions<A> {
87 /// Ask for the right to read the data, as [`FileOptions::read`](super::FileOptions::read) does.
88 pub fn read(self) -> ByIdOptions<A::WithRead> {
89 self.with_access()
90 }
91
92 /// Ask for the right to write the data, as [`FileOptions::write`](super::FileOptions::write) does.
93 pub fn write(self) -> ByIdOptions<A::WithWrite> {
94 self.with_access()
95 }
96
97 /// The rights besides the data access, as [`FileOptions::rights`](super::FileOptions::rights)
98 /// takes them. Calling this again replaces them.
99 pub fn rights(mut self, rights: FileRights) -> Self {
100 self.rights = rights;
101 self
102 }
103
104 /// What other opens may request while this handle is open (`dwShareMode`), as
105 /// [`FileOptions::share`](super::FileOptions::share) takes it. Calling this again replaces it.
106 pub fn share(mut self, share: ShareMode) -> Self {
107 self.share = share;
108 self
109 }
110
111 /// Open a reparse point itself and not what it points to (`FILE_FLAG_OPEN_REPARSE_POINT`). The
112 /// page says the flag is ignored for a file that is not a reparse point.
113 pub fn open_reparse_point(mut self, open_reparse_point: bool) -> Self {
114 self.open_reparse_point = open_reparse_point;
115 self
116 }
117
118 /// Ask that the data not be brought back from remote storage (`FILE_FLAG_OPEN_NO_RECALL`), as
119 /// [`FileOptions::open_no_recall`](super::FileOptions::open_no_recall) does.
120 pub fn open_no_recall(mut self, open_no_recall: bool) -> Self {
121 self.open_no_recall = open_no_recall;
122 self
123 }
124
125 /// Open for overlapped I/O (`FILE_FLAG_OVERLAPPED`), as
126 /// [`FileOptions::overlapped`](super::FileOptions::overlapped) describes.
127 pub fn overlapped(mut self, overlapped: bool) -> Self {
128 self.overlapped = overlapped;
129 self
130 }
131
132 /// The random-access hint (`FILE_FLAG_RANDOM_ACCESS`), as
133 /// [`FileOptions::random_access`](super::FileOptions::random_access) describes.
134 pub fn random_access(mut self, random_access: bool) -> Self {
135 self.random_access = random_access;
136 self
137 }
138
139 /// The sequential-scan hint (`FILE_FLAG_SEQUENTIAL_SCAN`), as
140 /// [`FileOptions::sequential_scan`](super::FileOptions::sequential_scan) describes.
141 pub fn sequential_scan(mut self, sequential_scan: bool) -> Self {
142 self.sequential_scan = sequential_scan;
143 self
144 }
145
146 fn with_access<A2: DataAccess>(self) -> ByIdOptions<A2> {
147 ByIdOptions {
148 access: PhantomData,
149 rights: self.rights,
150 share: self.share,
151 open_reparse_point: self.open_reparse_point,
152 open_no_recall: self.open_no_recall,
153 overlapped: self.overlapped,
154 random_access: self.random_access,
155 sequential_scan: self.sequential_scan,
156 }
157 }
158
159 /// `dwDesiredAccess`: the data rights `A` names and the other rights.
160 fn access(self) -> u32 {
161 data_rights::<A>() | self.rights.bits()
162 }
163
164 /// `dwFlagsAndAttributes`: `mode`, the buffering mode's flags, and the flags the options name.
165 fn flags(self, mode: u32) -> u32 {
166 let mut flags = mode;
167 if self.open_reparse_point {
168 flags |= FILE_FLAG_OPEN_REPARSE_POINT;
169 }
170 if self.open_no_recall {
171 flags |= FILE_FLAG_OPEN_NO_RECALL;
172 }
173 if self.overlapped {
174 flags |= FILE_FLAG_OVERLAPPED;
175 }
176 if self.random_access {
177 flags |= FILE_FLAG_RANDOM_ACCESS;
178 }
179 if self.sequential_scan {
180 flags |= FILE_FLAG_SEQUENTIAL_SCAN;
181 }
182 flags
183 }
184}
185
186/// The request to open a file by identifier, built by [`OpenedFile::request_by_id`] and made by
187/// [`perform`](Self::perform). It holds the hint, shared, so the handle it was built from can be
188/// closed first; it is `Send`, so it can be performed on a thread that can afford to block; and it
189/// can be performed more than once, each time opening the file again.
190#[derive(Debug)]
191#[must_use = "an unperformed request opens nothing"]
192pub struct ByIdRequest<B: Buffering = Cached, A: DataAccess = ReadWrite> {
193 request: OpenFileByIdentifier,
194 identifier: FileIdentifier,
195 rights: FileRights,
196 share: ShareMode,
197 mode: PhantomData<fn() -> (B, A)>,
198}
199
200impl<B: Buffering, A: DataAccess> ByIdRequest<B, A> {
201 /// The identifier this request opens.
202 #[must_use]
203 pub fn identifier(&self) -> FileIdentifier {
204 self.identifier
205 }
206
207 /// The hint whose volume the identifier is opened on, shared with its other holders.
208 #[must_use]
209 pub fn volume_hint(&self) -> &SharedHandle {
210 self.request.volume_hint()
211 }
212
213 /// Open the file, on the calling thread. **This blocks for as long as `OpenFileById` does.**
214 ///
215 /// # Errors
216 ///
217 /// - [`SharingViolation`](super::SharingViolation) (as an [`io::ErrorKind::ResourceBusy`] error),
218 /// naming the file by [`OpenedBy::Identifier`], when the file is open in a way that conflicts
219 /// with this open's access or sharing.
220 /// - For a [`FileIdentifier::Extended`], [`IdentityMismatch`] (as an
221 /// [`io::ErrorKind::InvalidInput`] error) when the file opened is not the identity asked for,
222 /// which is a hint on another volume; the handle is closed. An error reading the opened file's
223 /// identity is returned as it came.
224 /// - The path open's checks, [`RemoteDevice`](super::RemoteDevice) and
225 /// [`UnknownLocation`](super::UnknownLocation), made on the opened handle by the same code. No
226 /// test reaches them by identifier: the page lists SMB as not supported, so an open by
227 /// identifier on a share is the kernel's to refuse, and the call opens only a file on a file
228 /// system, whose final path is readable.
229 /// - Otherwise whatever `OpenFileById` reports, unaltered. The page names one, `ERROR_ACCESS_DENIED`
230 /// for a file pending deletion; an identifier that names nothing is the kernel's error, and the
231 /// page does not say which.
232 pub fn perform(&self) -> io::Result<OpenedFile<B, A>> {
233 let handle = self.request.perform().map_err(|error| {
234 refine_sharing_by(
235 error.to_io_error(),
236 || OpenedBy::Identifier(self.identifier),
237 data_access::<A>(),
238 self.rights,
239 self.share,
240 )
241 })?;
242 let handle = SharedHandle::from(handle);
243 if let FileIdentifier::Extended(asked) = self.identifier {
244 let opened = FileId::of(&handle)?;
245 if opened != asked {
246 return Err(io::Error::new(
247 io::ErrorKind::InvalidInput,
248 IdentityMismatch { asked, opened },
249 ));
250 }
251 }
252 OpenedFile::admit(handle)
253 }
254}
255
256impl<B: Buffering, A: DataAccess> OpenedFile<B, A> {
257 /// Open the file `identifier` names on the volume `volume_hint` is on, as `options` say, in
258 /// buffering mode `B`: [`request_by_id`](Self::request_by_id) and then
259 /// [`perform`](ByIdRequest::perform), one after the other on this thread.
260 ///
261 /// ```
262 /// use win_durable_ioring::windows_backend::{ByIdOptions, Cached, FileOptions, OpenedFile};
263 /// use win_durable_ioring::Wtf16String;
264 ///
265 /// let path = std::env::temp_dir().join(format!("by-id-doc-{}.tmp", std::process::id()));
266 /// std::fs::write(&path, b"x")?;
267 /// let name = Wtf16String::from_os_str(path.as_os_str());
268 /// let file = OpenedFile::<Cached, _>::open(name, FileOptions::new().read())?;
269 ///
270 /// // Any handle on the volume is a hint; here, the file's own.
271 /// let again = OpenedFile::<Cached, _>::open_by_id(
272 /// file.handle().clone(),
273 /// file.file_id()?,
274 /// ByIdOptions::new().read(),
275 /// )?;
276 /// assert_eq!(again.file_id()?, file.file_id()?);
277 /// # drop((file, again));
278 /// # std::fs::remove_file(&path)?;
279 /// # Ok::<(), Box<dyn std::error::Error>>(())
280 /// ```
281 ///
282 /// **An identifier can name a directory, and it is opened.** `FILE_FLAG_BACKUP_SEMANTICS`, which
283 /// the page says a directory needs, is not in v1, and an open by identifier was observed to reach a
284 /// directory without it. It is not refused: a target made from it faults every read and write with
285 /// the kernel's error, and a write that faulted is covered by no durability report, so the commits
286 /// it completes vouch for nothing. On a directory the data rights are `FILE_LIST_DIRECTORY` and
287 /// `FILE_ADD_FILE`, the same bits, and that is what the lent handle carries. What a flush of a
288 /// directory makes durable no page says. The design notes' `DI-D-72` has the measurement.
289 ///
290 /// # Errors
291 ///
292 /// Those of [`perform`](ByIdRequest::perform).
293 pub fn open_by_id(
294 volume_hint: impl Into<SharedHandle>,
295 identifier: impl Into<FileIdentifier>,
296 options: ByIdOptions<A>,
297 ) -> io::Result<Self> {
298 Self::request_by_id(volume_hint, identifier, options).perform()
299 }
300
301 /// The request to open the file `identifier` names on the volume `volume_hint` is on, built here
302 /// and made wherever the caller chooses with [`perform`](ByIdRequest::perform). Nothing is opened
303 /// and nothing blocks. The hint is held, shared, and not duplicated.
304 pub fn request_by_id(
305 volume_hint: impl Into<SharedHandle>,
306 identifier: impl Into<FileIdentifier>,
307 options: ByIdOptions<A>,
308 ) -> ByIdRequest<B, A> {
309 let identifier = identifier.into();
310 let request = OpenFileByIdentifier::new(volume_hint, identifier.to_request())
311 .with_desired_access(options.access())
312 .with_share_mode(options.share.bits())
313 .with_flags_and_attributes(options.flags(B::FLAGS));
314 ByIdRequest {
315 request,
316 identifier,
317 rights: options.rights,
318 share: options.share,
319 mode: PhantomData,
320 }
321 }
322}
323
324/// An open by a [`FileIdentifier::Extended`] opened a file whose identity is not the one asked for:
325/// the call takes the volume from the hint and not from the [`FileId`], so a hint on another volume
326/// opens whatever has that identifier there, or nothing. The handle is closed.
327///
328/// It is the cause of an [`io::ErrorKind::InvalidInput`] error from [`OpenedFile::open_by_id`],
329/// reached with [`io::Error::get_ref`] and a downcast.
330#[derive(Clone, Copy, Debug, PartialEq, Eq)]
331pub struct IdentityMismatch {
332 /// The identity the open was asked for.
333 pub asked: FileId,
334 /// The identity of the file it opened.
335 pub opened: FileId,
336}
337
338impl fmt::Display for IdentityMismatch {
339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340 write!(
341 f,
342 "the open by identifier asked for {:?} and opened {:?}: the volume hint is not on the \
343 volume the identity names",
344 self.asked, self.opened
345 )
346 }
347}
348
349impl std::error::Error for IdentityMismatch {}