windows_namespace_request_sys/final_path.rs
1// Copyright (c) Mike Grier.
2
3//! The `GetFinalPathNameByHandleW` entry.
4//!
5//! Entry 7 of the audited catalogue, and the one with the **strongest offload
6//! evidence**. Globazog reaches it through `std::fs::canonicalize`, which it
7//! performs on its *submitting* thread once per root -- a full `CreateFileW`
8//! plus this call plus `CloseHandle`, with unbounded latency on a network path
9//! -- and repeats per reparse-point candidate on a worker. It is first-class
10//! here, not second-tier.
11//!
12//! # The call reports a length, and the caller grows the buffer
13//!
14//! Unlike [`crate::query`], this call *does* report a length -- but with a
15//! twist that is easy to get wrong. On success it returns the number of
16//! characters written, **excluding** the terminating NUL. When the buffer is
17//! too small it returns the size required **including** the NUL, and does not
18//! set a failure code the caller would recognise as "try again bigger". The
19//! two returns are distinguished by comparing against the buffer size, and this
20//! entry does that retry itself rather than handing a caller a raw length it
21//! must interpret.
22//!
23//! The retry is bounded. A path cannot grow without limit between attempts, so
24//! an unbounded loop could only spin on a pathological or hostile filesystem;
25//! [`FinalPathError::Unstable`] reports that rather than hanging a worker.
26
27use std::fmt;
28
29use windows_sys::Win32::Storage::FileSystem::{
30 FILE_NAME_NORMALIZED, FILE_NAME_OPENED, GETFINALPATHNAMEBYHANDLE_FLAGS,
31 GetFinalPathNameByHandleW, VOLUME_NAME_DOS, VOLUME_NAME_GUID, VOLUME_NAME_NONE, VOLUME_NAME_NT,
32};
33use wtf_string::Wtf16String;
34
35use win_shared_os_owned_handle::SharedHandle;
36
37use crate::handle::SharedRaw;
38use crate::outcome::{Win32Error, perform_nonzero};
39
40/// How many times the buffer is grown before the result is called unstable.
41///
42/// One retry is the expected path: the first attempt learns the size and the
43/// second uses it. More than this means the answer changed under us repeatedly.
44const MAX_ATTEMPTS: usize = 8;
45
46/// The buffer size the first attempt uses, in characters.
47///
48/// `MAX_PATH`, which is enough for the overwhelming majority of paths, so the
49/// common case costs one call rather than two.
50const FIRST_ATTEMPT_CHARS: usize = 260;
51
52/// Which form of the final path to report.
53///
54/// A newtype over the flags rather than an enum, because the value combines a
55/// volume-name choice with a path-form choice and Windows may define more.
56///
57/// # Example
58///
59/// ```
60/// use windows_namespace_request_sys::final_path::FinalPathFlags;
61///
62/// // What the watcher uses, and the default here.
63/// let default = FinalPathFlags::DEFAULT;
64/// assert_eq!(default, FinalPathFlags::VOLUME_NAME_DOS | FinalPathFlags::NAME_NORMALIZED);
65///
66/// // VOLUME_NAME_DOS and FILE_NAME_NORMALIZED are both zero: they are the
67/// // defaults Windows applies when no opposing bit is set, not flags that can
68/// // be observed as present.
69/// assert_eq!(default.bits(), 0);
70///
71/// // The alternatives are the ones that carry bits.
72/// assert_ne!(FinalPathFlags::VOLUME_NAME_GUID.bits(), 0);
73/// ```
74#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
75pub struct FinalPathFlags(GETFINALPATHNAMEBYHANDLE_FLAGS);
76
77impl FinalPathFlags {
78 /// A drive letter, if the volume has one. Windows' default.
79 pub const VOLUME_NAME_DOS: Self = Self(VOLUME_NAME_DOS);
80 /// A volume GUID path, which has no dependence on drive-letter mappings.
81 pub const VOLUME_NAME_GUID: Self = Self(VOLUME_NAME_GUID);
82 /// The NT device path.
83 pub const VOLUME_NAME_NT: Self = Self(VOLUME_NAME_NT);
84 /// No volume component at all.
85 pub const VOLUME_NAME_NONE: Self = Self(VOLUME_NAME_NONE);
86 /// The normalised form of the path. Windows' default.
87 pub const NAME_NORMALIZED: Self = Self(FILE_NAME_NORMALIZED);
88 /// The path as it was opened, which may not be normalised.
89 pub const NAME_OPENED: Self = Self(FILE_NAME_OPENED);
90
91 /// `VOLUME_NAME_DOS | FILE_NAME_NORMALIZED`, which is what the audited
92 /// watcher relies on.
93 pub const DEFAULT: Self = Self(VOLUME_NAME_DOS | FILE_NAME_NORMALIZED);
94
95 /// Wraps a raw flags value.
96 #[must_use]
97 pub const fn from_bits(bits: GETFINALPATHNAMEBYHANDLE_FLAGS) -> Self {
98 Self(bits)
99 }
100
101 /// The raw flags value.
102 #[must_use]
103 pub const fn bits(self) -> GETFINALPATHNAMEBYHANDLE_FLAGS {
104 self.0
105 }
106}
107
108impl std::ops::BitOr for FinalPathFlags {
109 type Output = Self;
110
111 fn bitor(self, other: Self) -> Self {
112 Self(self.0 | other.0)
113 }
114}
115
116/// Why a final path could not be resolved.
117#[derive(Debug)]
118#[non_exhaustive]
119pub enum FinalPathError {
120 /// Windows refused the call, with the raw code unaltered.
121 Win32(Win32Error),
122 /// The required size kept changing, so the retry was abandoned.
123 ///
124 /// A path does not normally grow between two calls a microsecond apart, so
125 /// this means something pathological rather than a transient. It is
126 /// reported rather than looped on, because spinning here would hang the
127 /// worker that a consumer moved this call onto in the first place.
128 Unstable {
129 /// How many attempts were made before giving up.
130 attempts: usize,
131 },
132}
133
134impl fmt::Display for FinalPathError {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 match self {
137 Self::Win32(error) => write!(f, "GetFinalPathNameByHandleW: {error}"),
138 Self::Unstable { attempts } => write!(
139 f,
140 "GetFinalPathNameByHandleW: the required size changed on each of {attempts} attempts"
141 ),
142 }
143 }
144}
145
146impl std::error::Error for FinalPathError {
147 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
148 match self {
149 Self::Win32(error) => Some(error),
150 Self::Unstable { .. } => None,
151 }
152 }
153}
154
155impl From<Win32Error> for FinalPathError {
156 fn from(error: Win32Error) -> Self {
157 Self::Win32(error)
158 }
159}
160
161/// An owned, marshalable parameter set for `GetFinalPathNameByHandleW`.
162///
163/// # Example
164///
165/// ```
166/// use std::fs;
167///
168/// use windows_namespace_request_sys::final_path::QueryFinalPath;
169///
170/// let path = std::env::temp_dir().join(format!("wnrs-fp-{}.tmp", std::process::id()));
171/// fs::write(&path, b"example")?;
172/// let file = fs::File::open(&path)?;
173///
174/// // Built here; it would resolve identically on a worker, which is the point:
175/// // Globazog performs this on its submitting thread today.
176/// let resolved = QueryFinalPath::new(file)
177/// .perform()?
178/// .to_string_lossy();
179///
180/// // The result is a verbatim path, so it names the object unambiguously.
181/// assert!(resolved.starts_with(r"\\?\"), "unexpected: {resolved}");
182/// assert!(resolved.ends_with(".tmp"), "unexpected: {resolved}");
183/// # fs::remove_file(&path)?;
184/// # Ok::<(), Box<dyn std::error::Error>>(())
185/// ```
186#[derive(Clone, Debug)]
187#[must_use = "an unperformed request resolves nothing"]
188pub struct QueryFinalPath {
189 handle: SharedHandle,
190 flags: FinalPathFlags,
191}
192
193impl QueryFinalPath {
194 /// Begins a request against `handle`, in the form the audited watcher uses.
195 pub fn new(handle: impl Into<SharedHandle>) -> Self {
196 Self {
197 handle: handle.into(),
198 flags: FinalPathFlags::DEFAULT,
199 }
200 }
201
202 /// Sets `dwFlags`.
203 pub fn with_flags(mut self, flags: FinalPathFlags) -> Self {
204 self.flags = flags;
205 self
206 }
207
208 /// The handle being resolved, shared with its other holders.
209 pub fn handle(&self) -> &SharedHandle {
210 &self.handle
211 }
212
213 /// The flags the call will use.
214 #[must_use]
215 pub fn flags(&self) -> FinalPathFlags {
216 self.flags
217 }
218
219 /// Performs the call on the calling thread, growing the buffer as needed.
220 ///
221 /// # Errors
222 ///
223 /// Returns [`FinalPathError::Win32`] with the raw code unaltered, or
224 /// [`FinalPathError::Unstable`] if the required size kept changing.
225 pub fn perform(&self) -> Result<Wtf16String, FinalPathError> {
226 let mut capacity = FIRST_ATTEMPT_CHARS;
227
228 for _ in 0..MAX_ATTEMPTS {
229 let mut buffer = Wtf16String::with_capacity(capacity);
230 let requested = u32::try_from(capacity).unwrap_or(u32::MAX);
231
232 let written = perform_nonzero(|| {
233 // SAFETY: the handle is one this request holds and keeps
234 // open across the call, and the buffer is writable for
235 // `requested` characters. The buffer's invariant is restored
236 // below before it is observed.
237 unsafe {
238 GetFinalPathNameByHandleW(
239 self.handle.raw(),
240 buffer.as_mut_ptr(),
241 requested,
242 self.flags.bits(),
243 )
244 }
245 })?;
246
247 let written = written as usize;
248 if written < capacity {
249 // Success: `written` excludes the terminator, and Windows wrote
250 // that many characters plus one.
251 // SAFETY: exactly `written` content characters were written,
252 // within the requested capacity.
253 unsafe { buffer.set_len_from_ffi(written) };
254 return Ok(buffer);
255 }
256
257 // The buffer was too small, and `written` is the size *including*
258 // the terminator. Nothing usable was written, so the buffer is
259 // dropped rather than observed.
260 capacity = written;
261 }
262
263 Err(FinalPathError::Unstable {
264 attempts: MAX_ATTEMPTS,
265 })
266 }
267}
268
269impl crate::request::Request for QueryFinalPath {
270 type Error = FinalPathError;
271 type Output = Wtf16String;
272
273 fn perform(&self) -> Result<Wtf16String, FinalPathError> {
274 Self::perform(self)
275 }
276}
277
278#[cfg(test)]
279mod tests;