1use std::path::{Path, PathBuf};
9
10pub fn open_files_under(dir: &Path) -> Vec<PathBuf> {
16 let dir = dir
17 .canonicalize()
18 .unwrap_or_else(|error| panic!("canonicalize {}: {error}", dir.display()));
19 #[cfg(not(windows))]
20 let open = open_file_paths().into_iter().filter(|path| path.starts_with(&dir)).collect();
21 #[cfg(windows)]
22 let open = open_file_paths_under(&dir);
23 open
24}
25
26pub fn assert_no_open_files_under(dir: &Path) {
28 let open = open_files_under(dir);
29 assert!(open.is_empty(), "files still open under {}: {open:#?}", dir.display());
30}
31
32#[cfg(any(target_os = "macos", target_os = "ios"))]
33fn open_file_paths() -> Vec<PathBuf> {
34 use std::os::fd::BorrowedFd;
35 use std::os::unix::ffi::OsStringExt;
36
37 let descriptors: Vec<std::os::fd::RawFd> = std::fs::read_dir("/dev/fd")
38 .expect("list /dev/fd")
39 .map(|entry| {
40 entry
41 .expect("read /dev/fd entry")
42 .file_name()
43 .to_str()
44 .expect("descriptor name is UTF-8")
45 .parse()
46 .expect("descriptor name is a number")
47 })
48 .collect();
49 descriptors
50 .into_iter()
51 .filter_map(|fd| {
52 let fd = unsafe { BorrowedFd::borrow_raw(fd) };
56 let stat = match rustix::fs::fstat(fd) {
57 Ok(stat) => stat,
58 Err(rustix::io::Errno::BADF) => {
59 tracing::debug!(?fd, "descriptor closed during open-file enumeration");
60 return None;
61 }
62 Err(error) => panic!("inspect descriptor {fd:?}: {error}"),
63 };
64 if rustix::fs::FileType::from_raw_mode(stat.st_mode) == rustix::fs::FileType::Directory
65 {
66 return None;
67 }
68 let path = match rustix::fs::getpath(fd) {
69 Ok(path) => path,
70 Err(rustix::io::Errno::BADF | rustix::io::Errno::NOENT) => {
71 tracing::debug!(?fd, "descriptor has no current file path");
74 return None;
75 }
76 Err(error) => panic!("resolve descriptor {fd:?}: {error}"),
77 };
78 Some(PathBuf::from(std::ffi::OsString::from_vec(path.into_bytes())))
79 })
80 .collect()
81}
82
83#[cfg(any(target_os = "linux", target_os = "android"))]
84fn open_file_paths() -> Vec<PathBuf> {
85 std::fs::read_dir("/proc/self/fd")
86 .expect("list /proc/self/fd")
87 .filter_map(|entry| {
88 let link = entry.expect("read /proc/self/fd entry").path();
89 let metadata = match std::fs::metadata(&link) {
91 Ok(metadata) => metadata,
92 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
93 tracing::debug!(path = %link.display(), "descriptor closed during open-file enumeration");
94 return None;
95 }
96 Err(error) => panic!("inspect descriptor {}: {error}", link.display()),
97 };
98 if metadata.is_dir() {
99 return None;
100 }
101 match std::fs::read_link(&link) {
102 Ok(path) => Some(path),
103 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
104 tracing::debug!(path = %link.display(), "descriptor closed during open-file enumeration");
105 None
106 }
107 Err(error) => panic!("resolve descriptor {}: {error}", link.display()),
108 }
109 })
110 .collect()
111}
112
113#[cfg(windows)]
136fn open_file_paths_under(dir: &Path) -> Vec<PathBuf> {
137 use windows_sys::Win32::Storage::FileSystem::{
138 FILE_APPEND_DATA, FILE_READ_DATA, FILE_WRITE_DATA,
139 };
140
141 const DATA_ACCESS: u32 = FILE_READ_DATA | FILE_WRITE_DATA | FILE_APPEND_DATA;
142 let nt_dir = nt_directory_name(dir);
143 let file_type = windows_handles::file_object_type();
144 let mut name = vec![0u16; 32_768];
145 windows_handles::snapshot()
146 .into_iter()
147 .filter(|entry| {
148 entry.object_type_index == file_type && entry.granted_access & DATA_ACCESS != 0
149 })
150 .filter_map(|entry| {
151 let handle = entry.handle_value;
152 let read = |name: &mut [u16]| match file_under(handle, dir, &nt_dir, name) {
153 Err(unread) if unread.closed() => {
154 tracing::debug!(
155 ?handle,
156 operation = unread.operation,
157 "handle closed during open-file enumeration"
158 );
159 Ok(None)
160 }
161 read => read,
162 };
163 match read(&mut name).or_else(|first| read(&mut name).map_err(|again| (first, again))) {
164 Ok(path) => path,
165 Err((first, again)) => panic!(
166 "{} {handle:?} (granted access {:#x}): {}; read again, {}: {}",
167 first.operation,
168 entry.granted_access,
169 first.error,
170 again.operation,
171 again.error
172 ),
173 }
174 })
175 .collect()
176}
177
178#[cfg(windows)]
180struct UnreadHandle {
181 operation: &'static str,
182 error: std::io::Error,
183}
184
185#[cfg(windows)]
186impl UnreadHandle {
187 fn last(operation: &'static str) -> Self {
188 Self { operation, error: std::io::Error::last_os_error() }
189 }
190
191 fn closed(&self) -> bool {
193 self.error.raw_os_error()
194 == Some(windows_sys::Win32::Foundation::ERROR_INVALID_HANDLE as i32)
195 }
196}
197
198#[cfg(windows)]
201fn file_under(
202 handle: windows_sys::Win32::Foundation::HANDLE,
203 dir: &Path,
204 nt_dir: &str,
205 name: &mut [u16],
206) -> Result<Option<PathBuf>, UnreadHandle> {
207 use windows_sys::Win32::Foundation::{ERROR_ACCESS_DENIED, NO_ERROR};
208 use windows_sys::Win32::Storage::FileSystem::{
209 GetFileInformationByHandle, GetFileType, BY_HANDLE_FILE_INFORMATION,
210 FILE_ATTRIBUTE_DIRECTORY, FILE_TYPE_DISK, FILE_TYPE_UNKNOWN,
211 };
212
213 unsafe {
216 let kind = GetFileType(handle);
217 if kind == FILE_TYPE_UNKNOWN {
218 let unread = UnreadHandle::last("inspect file type");
219 if unread.error.raw_os_error() == Some(NO_ERROR as i32) {
222 return Ok(None);
223 }
224 return Err(unread);
225 }
226 if kind != FILE_TYPE_DISK {
227 return Ok(None);
228 }
229 let nt_name = match nt_handle_name(handle, name) {
230 Ok(nt_name) => nt_name,
231 Err(error) => {
232 if error.raw_os_error() == Some(ERROR_ACCESS_DENIED as i32)
235 && delete_pending(handle)
236 {
237 tracing::debug!(?handle, "skipping a deleted file still open");
238 return Ok(None);
239 }
240 return Err(UnreadHandle { operation: "resolve file handle", error });
241 }
242 };
243 let Some(suffix) = nt_suffix_under(&nt_name, nt_dir) else {
244 return Ok(None);
245 };
246 let path = dir.join(suffix);
247 let mut information: BY_HANDLE_FILE_INFORMATION = std::mem::zeroed();
248 if GetFileInformationByHandle(handle, &mut information) != 0 {
249 if information.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0 {
250 return Ok(None);
251 }
252 } else {
253 let unread = UnreadHandle::last("inspect file handle");
254 if unread.error.raw_os_error() != Some(ERROR_ACCESS_DENIED as i32) {
257 return Err(unread);
258 }
259 if std::fs::metadata(&path).is_ok_and(|metadata| metadata.is_dir()) {
260 return Ok(None);
261 }
262 }
263 Ok(Some(path))
264 }
265}
266
267#[cfg(windows)]
270fn nt_handle_name(
271 handle: windows_sys::Win32::Foundation::HANDLE,
272 buffer: &mut [u16],
273) -> std::io::Result<String> {
274 use windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED;
275 use windows_sys::Win32::Storage::FileSystem::{
276 GetFinalPathNameByHandleW, FILE_NAME_NORMALIZED, FILE_NAME_OPENED, VOLUME_NAME_NT,
277 };
278
279 let mut last = None;
280 for flags in [VOLUME_NAME_NT | FILE_NAME_NORMALIZED, VOLUME_NAME_NT | FILE_NAME_OPENED] {
281 let length = unsafe {
284 GetFinalPathNameByHandleW(handle, buffer.as_mut_ptr(), buffer.len() as u32, flags)
285 } as usize;
286 if length != 0 {
287 assert!(length < buffer.len(), "open file path exceeds its buffer");
288 return Ok(String::from_utf16_lossy(&buffer[..length]));
289 }
290 let error = std::io::Error::last_os_error();
291 if error.raw_os_error() != Some(ERROR_ACCESS_DENIED as i32) {
292 return Err(error);
293 }
294 tracing::debug!(?handle, flags, "file name refused; trying the opened name");
295 last = Some(error);
296 }
297 Err(last.expect("at least one naming attempt ran"))
298}
299
300#[cfg(windows)]
302fn nt_directory_name(dir: &Path) -> String {
303 use std::os::windows::fs::OpenOptionsExt;
304 use std::os::windows::io::AsRawHandle;
305 use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS;
306
307 let directory = std::fs::OpenOptions::new()
308 .access_mode(0)
309 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
310 .open(dir)
311 .unwrap_or_else(|error| panic!("open {}: {error}", dir.display()));
312 let mut buffer = vec![0u16; 32_768];
313 nt_handle_name(directory.as_raw_handle() as _, &mut buffer)
314 .unwrap_or_else(|error| panic!("resolve {}: {error}", dir.display()))
315}
316
317#[cfg(windows)]
320fn nt_suffix_under(nt_name: &str, nt_dir: &str) -> Option<PathBuf> {
321 let dir = nt_dir.trim_end_matches('\\');
322 let head = nt_name.get(..dir.len())?;
323 let rest = &nt_name[dir.len()..];
324 (head.eq_ignore_ascii_case(dir) && rest.starts_with('\\')).then(|| PathBuf::from(&rest[1..]))
325}
326
327#[cfg(windows)]
330fn delete_pending(handle: windows_sys::Win32::Foundation::HANDLE) -> bool {
331 use windows_sys::Win32::Storage::FileSystem::{
332 FileStandardInfo, GetFileInformationByHandleEx, FILE_STANDARD_INFO,
333 };
334 unsafe {
337 let mut information: FILE_STANDARD_INFO = std::mem::zeroed();
338 GetFileInformationByHandleEx(
339 handle,
340 FileStandardInfo,
341 (&mut information as *mut FILE_STANDARD_INFO).cast(),
342 std::mem::size_of::<FILE_STANDARD_INFO>() as u32,
343 ) != 0
344 && information.DeletePending
345 }
346}
347
348#[cfg(windows)]
349mod windows_handles {
350 use windows_sys::Wdk::System::Threading::{
351 NtQueryInformationProcess, ProcessHandleInformation,
352 };
353 use windows_sys::Win32::Foundation::{HANDLE, STATUS_INFO_LENGTH_MISMATCH};
354 use windows_sys::Win32::System::Threading::GetCurrentProcess;
355
356 #[repr(C)]
358 #[derive(Clone, Copy)]
359 pub(super) struct HandleEntry {
360 pub(super) handle_value: HANDLE,
361 handle_count: usize,
362 pointer_count: usize,
363 pub(super) granted_access: u32,
364 pub(super) object_type_index: u32,
365 handle_attributes: u32,
366 reserved: u32,
367 }
368
369 pub(super) fn snapshot() -> Vec<HandleEntry> {
371 let header = 2 * std::mem::size_of::<usize>();
375 let mut buffer: Vec<usize> = vec![0; 4096];
376 loop {
377 let bytes = std::mem::size_of_val(buffer.as_slice()) as u32;
378 let mut needed = 0u32;
379 let status = unsafe {
382 NtQueryInformationProcess(
383 GetCurrentProcess(),
384 ProcessHandleInformation,
385 buffer.as_mut_ptr().cast(),
386 bytes,
387 &mut needed,
388 )
389 };
390 if status == STATUS_INFO_LENGTH_MISMATCH {
391 let words =
392 (needed as usize).max(bytes as usize * 2) / std::mem::size_of::<usize>() + 1;
393 buffer = vec![0; words];
394 continue;
395 }
396 assert!(status >= 0, "snapshot this process's handles: status {status:#x}");
397 break;
398 }
399 let count = buffer[0];
400 unsafe {
403 std::slice::from_raw_parts(
404 buffer.as_ptr().cast::<u8>().add(header).cast::<HandleEntry>(),
405 count,
406 )
407 }
408 .to_vec()
409 }
410
411 pub(super) fn file_object_type() -> u32 {
414 use std::os::windows::io::AsRawHandle;
415 static FILE_TYPE: std::sync::OnceLock<u32> = std::sync::OnceLock::new();
416 *FILE_TYPE.get_or_init(|| {
417 let probe = std::fs::File::open(std::env::current_exe().expect("this executable"))
418 .expect("open this executable");
419 let value = probe.as_raw_handle() as HANDLE;
420 snapshot()
421 .into_iter()
422 .find(|entry| entry.handle_value == value)
423 .expect("an open file is in its process's snapshot")
424 .object_type_index
425 })
426 }
427}
428
429#[cfg(test)]
430#[path = "open_files_tests.rs"]
431mod tests;