win_durable_ioring/windows_backend/file_id.rs
1// Copyright (c) 2026 Mike Grier
2//! A file's identity, read from a handle to it (DI-15.1, DI-D-69).
3//!
4//! The identity is what `GetFileInformationByHandleEx` reports for its `FileIdInfo` class: the serial
5//! number of the volume the file is on and a 128-bit file identifier. The `FILE_ID_INFO` page says that
6//! "the file identifier and the volume serial number uniquely identify a file on a single computer".
7//! The call is the request crate's `QueryFileIdInfo`, given the handle shared and not a duplicate of it.
8//!
9//! The 128-bit form is the one this class reports. The page for `FILE_ID_DESCRIPTOR`, which
10//! `OpenFileById` takes, lists it as `ExtendedFileIdType`, beside the 64-bit `FileIdType` and
11//! `ObjectIdType`, and says it is "used on ReFS file systems"; an NTFS file's 16 bytes were observed
12//! to open it as that type (`D-31` in the request crate's design notes).
13//!
14//! [`FileIdentifier`] is what an open by identifier takes: a [`FileId`], or one of the page's two other
15//! kinds (DI-15.2, DI-D-70).
16
17use std::io;
18use std::ops::Range;
19
20use win_shared_os_owned_handle::SharedHandle;
21use windows_namespace_request_sys::file_id::{FileIdInfo, QueryFileIdInfo};
22use windows_namespace_request_sys::open_by_id::FileIdentifier as RequestIdentifier;
23
24use super::{Buffering, DataAccess, OpenedFile};
25
26#[cfg(test)]
27mod tests;
28
29/// The identity of a file on one computer: the serial number of its volume and its 128-bit file
30/// identifier, as `GetFileInformationByHandleEx`'s `FileIdInfo` class reported them.
31///
32/// Two values are equal when both fields are. What the fields mean beyond that is the call's: the
33/// identifier is carried as its 16 bytes and is not read as an integer, which would impose a byte
34/// order the call does not state.
35///
36/// # Example
37///
38/// ```
39/// use win_durable_ioring::windows_backend::{Cached, FileOptions, OpenedFile};
40/// use win_durable_ioring::Wtf16String;
41///
42/// let path = std::env::temp_dir().join(format!("file-id-doc-{}.tmp", std::process::id()));
43/// std::fs::write(&path, b"x")?;
44/// let name = || Wtf16String::from_os_str(path.as_os_str());
45///
46/// let first = OpenedFile::<Cached, _>::open(name(), FileOptions::new().read())?;
47/// let second = OpenedFile::<Cached, _>::open(name(), FileOptions::new().read().write())?;
48///
49/// // Two opens of one file report one identity.
50/// assert_eq!(first.file_id()?, second.file_id()?);
51/// # drop((first, second));
52/// # std::fs::remove_file(&path)?;
53/// # Ok::<(), Box<dyn std::error::Error>>(())
54/// ```
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
56pub struct FileId {
57 volume_serial_number: u64,
58 id: [u8; 16],
59}
60
61impl FileId {
62 /// An identity with these fields, for a caller that holds them from elsewhere, such as a
63 /// directory listing.
64 #[must_use]
65 pub const fn new(volume_serial_number: u64, id: [u8; 16]) -> Self {
66 Self {
67 volume_serial_number,
68 id,
69 }
70 }
71
72 /// The identity of the file `handle` is open on, read on the calling thread through the handle,
73 /// which is shared for the call and not duplicated.
74 ///
75 /// # Errors
76 ///
77 /// The code the call failed with, unaltered. An event, a pipe, `NUL` and a volume were observed
78 /// refused; the `FILE_ID_INFO` page does not list the codes, so none is promised.
79 pub fn of(handle: &SharedHandle) -> io::Result<Self> {
80 QueryFileIdInfo::new(handle.clone())
81 .perform()
82 .map(Self::from_info)
83 .map_err(|error| error.to_io_error())
84 }
85
86 /// `VolumeSerialNumber`: the serial number of the volume that contains the file.
87 #[must_use]
88 pub const fn volume_serial_number(&self) -> u64 {
89 self.volume_serial_number
90 }
91
92 /// `FileId`: the 128-bit file identifier, as its 16 bytes.
93 #[must_use]
94 pub const fn id(&self) -> [u8; 16] {
95 self.id
96 }
97
98 /// The request crate's value as this crate's, field for field.
99 fn from_info(info: FileIdInfo) -> Self {
100 Self::new(info.volume_serial_number(), info.file_id())
101 }
102}
103
104/// What an open by identifier names the file by: one of the three kinds the `FILE_ID_DESCRIPTOR` page
105/// lists. Each is opened on the volume of the hint the open is given, which is the only volume the
106/// call is told of.
107#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
108pub enum FileIdentifier {
109 /// `ExtendedFileIdType`: a [`FileId`]'s 128-bit identifier. A `FileId` also names a volume, which
110 /// the call does not take, so the opened file's identity is read and must be this one, volume
111 /// serial included; a hint on another volume is refused with
112 /// [`IdentityMismatch`](super::IdentityMismatch) and not answered with another volume's file.
113 Extended(FileId),
114 /// `FileIdType`: a 64-bit file ID, which `GetFileInformationByHandle` reports as
115 /// `nFileIndexHigh` and `nFileIndexLow`. It names no volume, so there is nothing to check.
116 FileId64(u64),
117 /// `ObjectIdType`: an object ID, as the 16 bytes of the `GUID` in the order Windows stores one in
118 /// memory, which is how `FSCTL_CREATE_OR_GET_OBJECT_ID` reports it (`FILE_OBJECTID_BUFFER`'s
119 /// `ObjectId`) and how `fsutil objectid` prints it. It names no volume.
120 ObjectId([u8; 16]),
121}
122
123impl From<FileId> for FileIdentifier {
124 fn from(id: FileId) -> Self {
125 Self::Extended(id)
126 }
127}
128
129impl FileIdentifier {
130 /// The identifier as the request crate's entry takes it.
131 pub(super) fn to_request(self) -> RequestIdentifier {
132 match self {
133 Self::Extended(id) => RequestIdentifier::ExtendedFileId(id.id()),
134 Self::FileId64(id) => RequestIdentifier::FileId(id),
135 Self::ObjectId(bytes) => RequestIdentifier::ObjectId(guid_value(bytes)),
136 }
137 }
138}
139
140/// Where each field of a `GUID` lies in its 16 bytes, as Windows lays one out: `Data1`, a `u32`, and
141/// `Data2` and `Data3`, each a `u16`, stored little-endian, then `Data4`, eight bytes in order.
142mod guid_layout {
143 use std::ops::Range;
144
145 pub const DATA1: Range<usize> = 0..4;
146 pub const DATA2: Range<usize> = 4..6;
147 pub const DATA3: Range<usize> = 6..8;
148}
149
150/// The `GUID` that `bytes` lay out, as the value the request crate's `ObjectId` takes: the `GUID`
151/// read as one big-endian number in the order of its canonical text form, which is what
152/// `GUID::from_u128` reads. The three little-endian fields are turned around and `Data4` is kept.
153fn guid_value(bytes: [u8; 16]) -> u128 {
154 let mut text_order = bytes;
155 for field in [guid_layout::DATA1, guid_layout::DATA2, guid_layout::DATA3] {
156 reverse(&mut text_order, field);
157 }
158 u128::from_be_bytes(text_order)
159}
160
161fn reverse(bytes: &mut [u8; 16], field: Range<usize>) {
162 bytes[field].reverse();
163}
164
165impl<B: Buffering, A: DataAccess> OpenedFile<B, A> {
166 /// The identity of the file, read through its handle on the calling thread, as [`FileId::of`]
167 /// reads it. Any data access reads it, [`NoData`](super::NoData) included.
168 ///
169 /// # Errors
170 ///
171 /// Those of [`FileId::of`].
172 pub fn file_id(&self) -> io::Result<FileId> {
173 FileId::of(self.handle())
174 }
175}