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 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;