windows_namespace_request_sys/full_path.rs
1// Copyright (c) Mike Grier.
2
3//! The `GetFullPathNameW` entry.
4//!
5//! Entry 9 of the audited catalogue, and the only one that takes neither a
6//! handle nor produces one.
7//!
8//! # What it solves, and what it leaves standing
9//!
10//! This call is **lexical**. It resolves relative components and `.`/`..`
11//! against the process current directory, and it touches no filesystem: it will
12//! happily resolve a path to something that does not exist.
13//!
14//! So it solves exactly one problem -- the process current directory is shared
15//! mutable state that any thread can change, so a relative path means something
16//! different depending on *when* it is resolved. Performing this on the
17//! submitting thread pins that meaning.
18//!
19//! It does **not** solve the session-relative drive-letter hazard, and saying
20//! so plainly matters more than the part it does solve. `GetFullPathNameW`
21//! never expands a drive letter, and a drive letter is resolved against the
22//! logon session of whatever token is in effect at open time. A path resolved
23//! here and opened on a worker under a captured token from another logon
24//! session can still name a different device. That hazard is open at the
25//! workspace level; this entry inherits it and does not close it.
26//!
27//! A consumer that wants the *final*, filesystem-verified path of an object
28//! wants [`crate::final_path`], which requires a handle and therefore an open.
29
30use std::fmt;
31
32use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW;
33use wtf_string::{Wtf16Str, Wtf16String};
34
35use crate::outcome::{Win32Error, perform_nonzero};
36
37/// How many times the buffer is grown before giving up.
38///
39/// As in [`crate::final_path`], one retry is the expected path; more means the
40/// answer is changing under us.
41const MAX_ATTEMPTS: usize = 8;
42
43/// The buffer size the first attempt uses, in characters.
44const FIRST_ATTEMPT_CHARS: usize = 260;
45
46/// Why a full path could not be resolved.
47///
48/// This mirrors [`crate::final_path::FinalPathError`] deliberately: the two
49/// entries share a retry shape, so they share a failure vocabulary. An earlier
50/// revision returned a synthesized `ERROR_INSUFFICIENT_BUFFER` for the unstable
51/// case, which left a caller unable to tell that apart from the same code
52/// arriving from Windows, and made this entry the one place in the crate that
53/// invented a code Win32 had not produced.
54#[derive(Debug)]
55#[non_exhaustive]
56pub enum FullPathError {
57 /// Windows refused the call, with the raw code unaltered.
58 Win32(Win32Error),
59 /// The required size kept changing, so the retry was abandoned.
60 ///
61 /// A path does not normally grow between two calls a microsecond apart, so
62 /// this means something pathological rather than a transient. It is
63 /// reported rather than looped on, because spinning here would hang the
64 /// worker that a consumer moved this call onto in the first place.
65 Unstable {
66 /// How many attempts were made before giving up.
67 attempts: usize,
68 },
69}
70
71impl fmt::Display for FullPathError {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match self {
74 Self::Win32(error) => write!(f, "GetFullPathNameW: {error}"),
75 Self::Unstable { attempts } => write!(
76 f,
77 "GetFullPathNameW: the required size changed on each of {attempts} attempts"
78 ),
79 }
80 }
81}
82
83impl std::error::Error for FullPathError {
84 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
85 match self {
86 Self::Win32(error) => Some(error),
87 Self::Unstable { .. } => None,
88 }
89 }
90}
91
92impl From<Win32Error> for FullPathError {
93 fn from(error: Win32Error) -> Self {
94 Self::Win32(error)
95 }
96}
97
98/// An owned, marshalable parameter set for `GetFullPathNameW`.
99///
100/// # Example
101///
102/// ```
103/// use windows_namespace_request_sys::full_path::ResolveFullPath;
104/// use wtf_string::Wtf16String;
105///
106/// // Lexical: `.` and `..` are resolved without touching the filesystem.
107/// let resolved = ResolveFullPath::new(Wtf16String::from(r"C:\Windows\System32\..\.\Temp"))
108/// .perform()?
109/// .to_string_lossy();
110///
111/// assert_eq!(resolved, r"C:\Windows\Temp");
112/// # Ok::<(), Box<dyn std::error::Error>>(())
113/// ```
114///
115/// # Example: it does not check existence
116///
117/// ```
118/// use windows_namespace_request_sys::full_path::ResolveFullPath;
119/// use wtf_string::Wtf16String;
120///
121/// // A path to nothing resolves perfectly happily, because the call is
122/// // lexical. A consumer wanting a verified path wants an open plus
123/// // GetFinalPathNameByHandleW instead.
124/// let resolved = ResolveFullPath::new(Wtf16String::from(r"C:\no-such-directory\..\file.txt"))
125/// .perform()?
126/// .to_string_lossy();
127///
128/// assert_eq!(resolved, r"C:\file.txt");
129/// # Ok::<(), Box<dyn std::error::Error>>(())
130/// ```
131#[derive(Clone, Debug)]
132#[must_use = "an unperformed request resolves nothing"]
133pub struct ResolveFullPath {
134 path: Wtf16String,
135}
136
137impl ResolveFullPath {
138 /// Begins a request to resolve `path`.
139 ///
140 /// Takes a raw path rather than a [`crate::path::PreparedPath`], because
141 /// preparation is what this call *performs*. Handing it an already-prepared
142 /// path would be resolving twice.
143 pub fn new(path: Wtf16String) -> Self {
144 Self { path }
145 }
146
147 /// The path this request will resolve.
148 #[must_use]
149 pub fn path(&self) -> &Wtf16Str {
150 &self.path
151 }
152
153 /// Performs the call on the calling thread, growing the buffer as needed.
154 ///
155 /// Resolution happens against the current directory of **whichever thread
156 /// performs this**, which is the one thing a caller must keep in mind: a
157 /// request built on a submitter and performed on a worker resolves against
158 /// the process current directory as it stands at *performance* time.
159 /// [`crate::path::prepare`] is the function for pinning that at
160 /// construction.
161 ///
162 /// # Errors
163 ///
164 /// Returns [`FullPathError::Win32`] with the raw Win32 code, unaltered, or
165 /// [`FullPathError::Unstable`] if the required size kept changing.
166 pub fn perform(&self) -> Result<Wtf16String, FullPathError> {
167 let mut capacity = FIRST_ATTEMPT_CHARS;
168
169 for _ in 0..MAX_ATTEMPTS {
170 let mut buffer = Wtf16String::with_capacity(capacity);
171 let requested = u32::try_from(capacity).unwrap_or(u32::MAX);
172
173 let written = perform_nonzero(|| {
174 // SAFETY: the input has no interior NUL by Wtf16String's own
175 // invariant for a terminated pointer, and the buffer is
176 // writable for `requested` characters. The buffer's invariant
177 // is restored below before it is observed.
178 unsafe {
179 GetFullPathNameW(
180 self.path.as_terminated_ptr(),
181 requested,
182 buffer.as_mut_ptr(),
183 core::ptr::null_mut(),
184 )
185 }
186 })?;
187
188 let written = written as usize;
189 if written < capacity {
190 // Success: `written` excludes the terminator.
191 // SAFETY: exactly `written` content characters were written,
192 // within the requested capacity.
193 unsafe { buffer.set_len_from_ffi(written) };
194 return Ok(buffer);
195 }
196
197 // Too small: `written` is the size required *including* the
198 // terminator, and nothing usable was written.
199 capacity = written;
200 }
201
202 // The required size kept changing across every attempt. Report it
203 // rather than looping, for the reason final_path gives: spinning here
204 // would hang the worker a consumer moved this call onto.
205 //
206 // Reported as its own variant rather than as a Win32 code. Windows has
207 // none for "and it kept happening", and the nearest candidate --
208 // `ERROR_INSUFFICIENT_BUFFER`, which each individual attempt really did
209 // hit -- is one Win32 can also return on its own, so borrowing it would
210 // leave a caller unable to tell the two apart.
211 Err(FullPathError::Unstable {
212 attempts: MAX_ATTEMPTS,
213 })
214 }
215}
216
217impl crate::request::Request for ResolveFullPath {
218 type Error = FullPathError;
219 type Output = Wtf16String;
220
221 fn perform(&self) -> Result<Wtf16String, FullPathError> {
222 Self::perform(self)
223 }
224}
225
226#[cfg(test)]
227mod tests;