Skip to main content

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}