Skip to main content

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 crate::handle::{CapturedHandle, HandleCaptureError};
36use crate::outcome::{Win32Error, perform_nonzero};
37
38/// How many times the buffer is grown before the result is called unstable.
39///
40/// One retry is the expected path: the first attempt learns the size and the
41/// second uses it. More than this means the answer changed under us repeatedly.
42const MAX_ATTEMPTS: usize = 8;
43
44/// The buffer size the first attempt uses, in characters.
45///
46/// `MAX_PATH`, which is enough for the overwhelming majority of paths, so the
47/// common case costs one call rather than two.
48const FIRST_ATTEMPT_CHARS: usize = 260;
49
50/// Which form of the final path to report.
51///
52/// A newtype over the flags rather than an enum, because the value combines a
53/// volume-name choice with a path-form choice and Windows may define more.
54///
55/// # Example
56///
57/// ```
58/// use windows_namespace_request_sys::final_path::FinalPathFlags;
59///
60/// // What the watcher uses, and the default here.
61/// let default = FinalPathFlags::DEFAULT;
62/// assert_eq!(default, FinalPathFlags::VOLUME_NAME_DOS | FinalPathFlags::NAME_NORMALIZED);
63///
64/// // VOLUME_NAME_DOS and FILE_NAME_NORMALIZED are both zero: they are the
65/// // defaults Windows applies when no opposing bit is set, not flags that can
66/// // be observed as present.
67/// assert_eq!(default.bits(), 0);
68///
69/// // The alternatives are the ones that carry bits.
70/// assert_ne!(FinalPathFlags::VOLUME_NAME_GUID.bits(), 0);
71/// ```
72#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
73pub struct FinalPathFlags(GETFINALPATHNAMEBYHANDLE_FLAGS);
74
75impl FinalPathFlags {
76    /// A drive letter, if the volume has one. Windows' default.
77    pub const VOLUME_NAME_DOS: Self = Self(VOLUME_NAME_DOS);
78    /// A volume GUID path, which has no dependence on drive-letter mappings.
79    pub const VOLUME_NAME_GUID: Self = Self(VOLUME_NAME_GUID);
80    /// The NT device path.
81    pub const VOLUME_NAME_NT: Self = Self(VOLUME_NAME_NT);
82    /// No volume component at all.
83    pub const VOLUME_NAME_NONE: Self = Self(VOLUME_NAME_NONE);
84    /// The normalised form of the path. Windows' default.
85    pub const NAME_NORMALIZED: Self = Self(FILE_NAME_NORMALIZED);
86    /// The path as it was opened, which may not be normalised.
87    pub const NAME_OPENED: Self = Self(FILE_NAME_OPENED);
88
89    /// `VOLUME_NAME_DOS | FILE_NAME_NORMALIZED`, which is what the audited
90    /// watcher relies on.
91    pub const DEFAULT: Self = Self(VOLUME_NAME_DOS | FILE_NAME_NORMALIZED);
92
93    /// Wraps a raw flags value.
94    #[must_use]
95    pub const fn from_bits(bits: GETFINALPATHNAMEBYHANDLE_FLAGS) -> Self {
96        Self(bits)
97    }
98
99    /// The raw flags value.
100    #[must_use]
101    pub const fn bits(self) -> GETFINALPATHNAMEBYHANDLE_FLAGS {
102        self.0
103    }
104}
105
106impl std::ops::BitOr for FinalPathFlags {
107    type Output = Self;
108
109    fn bitor(self, other: Self) -> Self {
110        Self(self.0 | other.0)
111    }
112}
113
114/// Why a final path could not be resolved.
115#[derive(Debug)]
116#[non_exhaustive]
117pub enum FinalPathError {
118    /// Windows refused the call, with the raw code unaltered.
119    Win32(Win32Error),
120    /// The required size kept changing, so the retry was abandoned.
121    ///
122    /// A path does not normally grow between two calls a microsecond apart, so
123    /// this means something pathological rather than a transient. It is
124    /// reported rather than looped on, because spinning here would hang the
125    /// worker that a consumer moved this call onto in the first place.
126    Unstable {
127        /// How many attempts were made before giving up.
128        attempts: usize,
129    },
130}
131
132impl fmt::Display for FinalPathError {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        match self {
135            Self::Win32(error) => write!(f, "GetFinalPathNameByHandleW: {error}"),
136            Self::Unstable { attempts } => write!(
137                f,
138                "GetFinalPathNameByHandleW: the required size changed on each of {attempts} attempts"
139            ),
140        }
141    }
142}
143
144impl std::error::Error for FinalPathError {
145    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
146        match self {
147            Self::Win32(error) => Some(error),
148            Self::Unstable { .. } => None,
149        }
150    }
151}
152
153impl From<Win32Error> for FinalPathError {
154    fn from(error: Win32Error) -> Self {
155        Self::Win32(error)
156    }
157}
158
159/// An owned, marshalable parameter set for `GetFinalPathNameByHandleW`.
160///
161/// # Example
162///
163/// ```
164/// use std::fs;
165/// use std::os::windows::io::AsHandle;
166///
167/// use windows_namespace_request_sys::final_path::QueryFinalPath;
168/// use windows_namespace_request_sys::CapturedHandle;
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(CapturedHandle::capture(file.as_handle())?)
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/// # drop(file);
184/// # fs::remove_file(&path)?;
185/// # Ok::<(), Box<dyn std::error::Error>>(())
186/// ```
187#[derive(Debug)]
188#[must_use = "an unperformed request resolves nothing"]
189pub struct QueryFinalPath {
190    handle: CapturedHandle,
191    flags: FinalPathFlags,
192}
193
194impl QueryFinalPath {
195    /// Begins a request against `handle`, in the form the audited watcher uses.
196    pub fn new(handle: CapturedHandle) -> Self {
197        Self {
198            handle,
199            flags: FinalPathFlags::DEFAULT,
200        }
201    }
202
203    /// Sets `dwFlags`.
204    pub fn with_flags(mut self, flags: FinalPathFlags) -> Self {
205        self.flags = flags;
206        self
207    }
208
209    /// The owned duplicate of the handle being resolved.
210    pub fn handle(&self) -> &CapturedHandle {
211        &self.handle
212    }
213
214    /// The flags the call will use.
215    #[must_use]
216    pub fn flags(&self) -> FinalPathFlags {
217        self.flags
218    }
219
220    /// Copies the request, duplicating the handle.
221    ///
222    /// # Errors
223    ///
224    /// Returns the handle-capture failure when the handle cannot be duplicated.
225    pub fn try_clone(&self) -> Result<Self, HandleCaptureError> {
226        Ok(Self {
227            handle: self.handle.try_clone()?,
228            flags: self.flags,
229        })
230    }
231
232    /// Performs the call on the calling thread, growing the buffer as needed.
233    ///
234    /// # Errors
235    ///
236    /// Returns [`FinalPathError::Win32`] with the raw code unaltered, or
237    /// [`FinalPathError::Unstable`] if the required size kept changing.
238    pub fn perform(&self) -> Result<Wtf16String, FinalPathError> {
239        let mut capacity = FIRST_ATTEMPT_CHARS;
240
241        for _ in 0..MAX_ATTEMPTS {
242            let mut buffer = Wtf16String::with_capacity(capacity);
243            let requested = u32::try_from(capacity).unwrap_or(u32::MAX);
244
245            let written = perform_nonzero(|| {
246                // SAFETY: the handle is a duplicate this request owns and keeps
247                // open across the call, and the buffer is writable for
248                // `requested` characters. The buffer's invariant is restored
249                // below before it is observed.
250                unsafe {
251                    GetFinalPathNameByHandleW(
252                        self.handle.raw(),
253                        buffer.as_mut_ptr(),
254                        requested,
255                        self.flags.bits(),
256                    )
257                }
258            })?;
259
260            let written = written as usize;
261            if written < capacity {
262                // Success: `written` excludes the terminator, and Windows wrote
263                // that many characters plus one.
264                // SAFETY: exactly `written` content characters were written,
265                // within the requested capacity.
266                unsafe { buffer.set_len_from_ffi(written) };
267                return Ok(buffer);
268            }
269
270            // The buffer was too small, and `written` is the size *including*
271            // the terminator. Nothing usable was written, so the buffer is
272            // dropped rather than observed.
273            capacity = written;
274        }
275
276        Err(FinalPathError::Unstable {
277            attempts: MAX_ATTEMPTS,
278        })
279    }
280}
281
282impl crate::request::Request for QueryFinalPath {
283    type Error = FinalPathError;
284    type Output = Wtf16String;
285
286    fn perform(&self) -> Result<Wtf16String, FinalPathError> {
287        Self::perform(self)
288    }
289}
290
291#[cfg(test)]
292mod tests;