1 //********************************************************* 2 // 3 // Copyright (c) Microsoft. All rights reserved. 4 // This code is licensed under the MIT License. 5 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 6 // ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 7 // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 8 // PARTICULAR PURPOSE AND NONINFRINGEMENT. 9 // 10 //********************************************************* 11 #ifndef __WIL_FILESYSTEM_INCLUDED 12 #define __WIL_FILESYSTEM_INCLUDED 13 14 #ifdef _KERNEL_MODE 15 #error This header is not supported in kernel-mode. 16 #endif 17 18 #include <new> 19 #include <combaseapi.h> // Needed for CoTaskMemFree() used in output of some helpers. 20 #include <winbase.h> // LocalAlloc 21 #include <PathCch.h> 22 #include "result.h" 23 #include "win32_helpers.h" 24 #include "resource.h" 25 26 namespace wil 27 { 28 //! Determines if a path is an extended length path that can be used to access paths longer than MAX_PATH. is_extended_length_path(_In_ PCWSTR path)29 inline bool is_extended_length_path(_In_ PCWSTR path) 30 { 31 return wcsncmp(path, L"\\\\?\\", 4) == 0; 32 } 33 34 //! Find the last segment of a path. Matches the behavior of shlwapi!PathFindFileNameW() 35 //! note, does not support streams being specified like PathFindFileNameW(), is that a bug or a feature? find_last_path_segment(_In_ PCWSTR path)36 inline PCWSTR find_last_path_segment(_In_ PCWSTR path) 37 { 38 auto const pathLength = wcslen(path); 39 // If there is a trailing slash ignore that in the search. 40 auto const limitedLength = ((pathLength > 0) && (path[pathLength - 1] == L'\\')) ? (pathLength - 1) : pathLength; 41 42 PCWSTR result; 43 auto const offset = FindStringOrdinal(FIND_FROMEND, path, static_cast<int>(limitedLength), L"\\", 1, TRUE); 44 if (offset == -1) 45 { 46 result = path + pathLength; // null terminator 47 } 48 else 49 { 50 result = path + offset + 1; // just past the slash 51 } 52 return result; 53 } 54 55 //! Determine if the file name is one of the special "." or ".." names. path_is_dot_or_dotdot(_In_ PCWSTR fileName)56 inline bool path_is_dot_or_dotdot(_In_ PCWSTR fileName) 57 { 58 return ((fileName[0] == L'.') && 59 ((fileName[1] == L'\0') || ((fileName[1] == L'.') && (fileName[2] == L'\0')))); 60 } 61 62 //! Returns the drive number, if it has one. Returns true if there is a drive number, false otherwise. Supports regular and extended length paths. try_get_drive_letter_number(_In_ PCWSTR path,_Out_ int * driveNumber)63 inline bool try_get_drive_letter_number(_In_ PCWSTR path, _Out_ int* driveNumber) 64 { 65 if (path[0] == L'\\' && path[1] == L'\\' && path[2] == L'?' && path[3] == L'\\') 66 { 67 path += 4; 68 } 69 if (path[0] && (path[1] == L':')) 70 { 71 if ((path[0] >= L'a') && (path[0] <= L'z')) 72 { 73 *driveNumber = path[0] - L'a'; 74 return true; 75 } 76 else if ((path[0] >= L'A') && (path[0] <= L'Z')) 77 { 78 *driveNumber = path[0] - L'A'; 79 return true; 80 } 81 } 82 *driveNumber = -1; 83 return false; 84 } 85 86 #if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) 87 88 // PathCch.h APIs are only in desktop API for now. 89 90 // Compute the substring in the input value that is the parent folder path. 91 // returns: 92 // true + parentPathLength - path has a parent starting at the beginning path and of parentPathLength length. 93 // false, no parent path, the input is a root path. try_get_parent_path_range(_In_ PCWSTR path,_Out_ size_t * parentPathLength)94 inline bool try_get_parent_path_range(_In_ PCWSTR path, _Out_ size_t* parentPathLength) 95 { 96 *parentPathLength = 0; 97 bool hasParent = false; 98 PCWSTR rootEnd; 99 if (SUCCEEDED(PathCchSkipRoot(path, &rootEnd)) && (*rootEnd != L'\0')) 100 { 101 auto const lastSegment = find_last_path_segment(path); 102 *parentPathLength = lastSegment - path; 103 hasParent = (*parentPathLength != 0); 104 } 105 return hasParent; 106 } 107 108 // Creates directories for the specified path, creating parent paths 109 // as needed. CreateDirectoryDeepNoThrow(PCWSTR path)110 inline HRESULT CreateDirectoryDeepNoThrow(PCWSTR path) WI_NOEXCEPT 111 { 112 if (::CreateDirectoryW(path, nullptr) == FALSE) 113 { 114 DWORD const lastError = ::GetLastError(); 115 if (lastError == ERROR_PATH_NOT_FOUND) 116 { 117 size_t parentLength; 118 if (try_get_parent_path_range(path, &parentLength)) 119 { 120 wistd::unique_ptr<wchar_t[]> parent(new (std::nothrow) wchar_t[parentLength + 1]); 121 RETURN_IF_NULL_ALLOC(parent.get()); 122 RETURN_IF_FAILED(StringCchCopyNW(parent.get(), parentLength + 1, path, parentLength)); 123 CreateDirectoryDeepNoThrow(parent.get()); // recurs 124 } 125 RETURN_IF_WIN32_BOOL_FALSE(::CreateDirectoryW(path, nullptr)); 126 } 127 else if (lastError != ERROR_ALREADY_EXISTS) 128 { 129 RETURN_WIN32(lastError); 130 } 131 } 132 return S_OK; 133 } 134 135 #ifdef WIL_ENABLE_EXCEPTIONS CreateDirectoryDeep(PCWSTR path)136 inline void CreateDirectoryDeep(PCWSTR path) 137 { 138 THROW_IF_FAILED(CreateDirectoryDeepNoThrow(path)); 139 } 140 #endif // WIL_ENABLE_EXCEPTIONS 141 142 //! A strongly typed version of the Win32 API GetFullPathNameW. 143 //! Return a path in an allocated buffer for handling long paths. 144 //! Optionally return the pointer to the file name part. 145 template <typename string_type, size_t stackBufferLength = 256> 146 HRESULT GetFullPathNameW(PCWSTR file, string_type& path, _Outptr_opt_ PCWSTR* filePart = nullptr) 147 { 148 wil::assign_null_to_opt_param(filePart); 149 const auto hr = AdaptFixedSizeToAllocatedResult<string_type, stackBufferLength>(path, 150 [&](_Out_writes_(valueLength) PWSTR value, size_t valueLength, _Out_ size_t* valueLengthNeededWithNull) -> HRESULT 151 { 152 // Note that GetFullPathNameW() is not limited to MAX_PATH 153 // but it does take a fixed size buffer. 154 *valueLengthNeededWithNull = ::GetFullPathNameW(file, static_cast<DWORD>(valueLength), value, nullptr); 155 RETURN_LAST_ERROR_IF(*valueLengthNeededWithNull == 0); 156 WI_ASSERT((*value != L'\0') == (*valueLengthNeededWithNull < valueLength)); 157 if (*valueLengthNeededWithNull < valueLength) 158 { 159 (*valueLengthNeededWithNull)++; // it fit, account for the null 160 } 161 return S_OK; 162 }); 163 if (SUCCEEDED(hr) && filePart) 164 { 165 *filePart = wil::find_last_path_segment(details::string_maker<string_type>::get(path)); 166 } 167 return hr; 168 } 169 170 #ifdef WIL_ENABLE_EXCEPTIONS 171 //! A strongly typed version of the Win32 API of GetFullPathNameW. 172 //! Return a path in an allocated buffer for handling long paths. 173 //! Optionally return the pointer to the file name part. 174 template <typename string_type = wil::unique_cotaskmem_string, size_t stackBufferLength = 256> 175 string_type GetFullPathNameW(PCWSTR file, _Outptr_opt_ PCWSTR* filePart = nullptr) 176 { 177 string_type result; 178 THROW_IF_FAILED((GetFullPathNameW<string_type, stackBufferLength>(file, result, filePart))); 179 return result; 180 } 181 #endif 182 183 enum class RemoveDirectoryOptions 184 { 185 None = 0, 186 KeepRootDirectory = 0x1 187 }; 188 DEFINE_ENUM_FLAG_OPERATORS(RemoveDirectoryOptions); 189 190 // If inputPath is a non-normalized name be sure to pass an extended length form to ensure 191 // it can be addressed and deleted. 192 inline HRESULT RemoveDirectoryRecursiveNoThrow(PCWSTR inputPath, RemoveDirectoryOptions options = RemoveDirectoryOptions::None) WI_NOEXCEPT 193 { 194 wil::unique_hlocal_string path; 195 PATHCCH_OPTIONS combineOptions = PATHCCH_NONE; 196 197 if (is_extended_length_path(inputPath)) 198 { 199 path = wil::make_hlocal_string_nothrow(inputPath); 200 RETURN_IF_NULL_ALLOC(path); 201 // PathAllocCombine will convert extended length paths to regular paths if shorter than 202 // MAX_PATH, avoid that behavior to provide access inputPath with non-normalized names. 203 combineOptions = PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH; 204 } 205 else 206 { 207 // For regular paths normalize here to get consistent results when searching and deleting. 208 RETURN_IF_FAILED(wil::GetFullPathNameW(inputPath, path)); 209 combineOptions = PATHCCH_ALLOW_LONG_PATHS; 210 } 211 212 wil::unique_hlocal_string searchPath; 213 RETURN_IF_FAILED(::PathAllocCombine(path.get(), L"*", combineOptions, &searchPath)); 214 215 WIN32_FIND_DATAW fd; 216 wil::unique_hfind findHandle(::FindFirstFileW(searchPath.get(), &fd)); 217 RETURN_LAST_ERROR_IF(!findHandle); 218 219 for (;;) 220 { 221 // skip "." and ".." 222 if (!(WI_IsFlagSet(fd.dwFileAttributes, FILE_ATTRIBUTE_DIRECTORY) && path_is_dot_or_dotdot(fd.cFileName))) 223 { 224 // Need to form an extended length path to provide the ability to delete paths > MAX_PATH 225 // and files with non-normalized names (dots or spaces at the end). 226 wil::unique_hlocal_string pathToDelete; 227 RETURN_IF_FAILED(::PathAllocCombine(path.get(), fd.cFileName, 228 PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH | PATHCCH_DO_NOT_NORMALIZE_SEGMENTS, &pathToDelete)); 229 if (WI_IsFlagSet(fd.dwFileAttributes, FILE_ATTRIBUTE_DIRECTORY)) 230 { 231 RemoveDirectoryOptions localOptions = options; 232 RETURN_IF_FAILED(RemoveDirectoryRecursiveNoThrow(pathToDelete.get(), WI_ClearFlag(localOptions, RemoveDirectoryOptions::KeepRootDirectory))); 233 } 234 else 235 { 236 // note: if pathToDelete is read-only this will fail, consider adding 237 // RemoveDirectoryOptions::RemoveReadOnly to enable this behavior. 238 RETURN_IF_WIN32_BOOL_FALSE(::DeleteFileW(pathToDelete.get())); 239 } 240 } 241 242 if (!::FindNextFileW(findHandle.get(), &fd)) 243 { 244 auto const err = ::GetLastError(); 245 if (err == ERROR_NO_MORE_FILES) 246 { 247 break; 248 } 249 RETURN_WIN32(err); 250 } 251 } 252 253 if (WI_IsFlagClear(options, RemoveDirectoryOptions::KeepRootDirectory)) 254 { 255 RETURN_IF_WIN32_BOOL_FALSE(::RemoveDirectoryW(path.get())); 256 } 257 return S_OK; 258 } 259 260 #ifdef WIL_ENABLE_EXCEPTIONS 261 inline void RemoveDirectoryRecursive(PCWSTR path, RemoveDirectoryOptions options = RemoveDirectoryOptions::None) 262 { 263 THROW_IF_FAILED(RemoveDirectoryRecursiveNoThrow(path, options)); 264 } 265 #endif // WIL_ENABLE_EXCEPTIONS 266 267 // Range based for that supports Win32 structures that use NextEntryOffset as the basis of traversing 268 // a result buffer that contains data. This is used in the following FileIO calls: 269 // FileStreamInfo, FILE_STREAM_INFO 270 // FileIdBothDirectoryInfo, FILE_ID_BOTH_DIR_INFO 271 // FileFullDirectoryInfo, FILE_FULL_DIR_INFO 272 // FileIdExtdDirectoryInfo, FILE_ID_EXTD_DIR_INFO 273 // ReadDirectoryChangesW, FILE_NOTIFY_INFORMATION 274 275 template <typename T> 276 struct next_entry_offset_iterator 277 { 278 // Fulfill std::iterator_traits requirements 279 using difference_type = ptrdiff_t; 280 using value_type = T; 281 using pointer = const T*; 282 using reference = const T&; 283 #ifdef _XUTILITY_ 284 using iterator_category = ::std::forward_iterator_tag; 285 #endif 286 current_next_entry_offset_iterator287 next_entry_offset_iterator(T *iterable = __nullptr) : current_(iterable) {} 288 289 // range based for requires operator!=, operator++ and operator* to do its work 290 // on the type returned from begin() and end(), provide those here. 291 bool operator!=(const next_entry_offset_iterator& other) const { return current_ != other.current_; } 292 293 next_entry_offset_iterator& operator++() 294 { 295 current_ = (current_->NextEntryOffset != 0) ? 296 reinterpret_cast<T *>(reinterpret_cast<unsigned char*>(current_) + current_->NextEntryOffset) : 297 __nullptr; 298 return *this; 299 } 300 301 next_entry_offset_iterator operator++(int) 302 { 303 auto copy = *this; 304 ++(*this); 305 return copy; 306 } 307 308 reference operator*() const WI_NOEXCEPT { return *current_; } 309 pointer operator->() const WI_NOEXCEPT { return current_; } 310 beginnext_entry_offset_iterator311 next_entry_offset_iterator<T> begin() { return *this; } endnext_entry_offset_iterator312 next_entry_offset_iterator<T> end() { return next_entry_offset_iterator<T>(); } 313 314 T* current_; 315 }; 316 317 template <typename T> create_next_entry_offset_iterator(T * p)318 next_entry_offset_iterator<T> create_next_entry_offset_iterator(T* p) 319 { 320 return next_entry_offset_iterator<T>(p); 321 } 322 323 #pragma region Folder Watcher 324 // Example use in exception based code: 325 // auto watcher = wil::make_folder_watcher(folder.Path().c_str(), true, wil::allChangeEvents, []() 326 // { 327 // // respond 328 // }); 329 // 330 // Example use in result code based code: 331 // wil::unique_folder_watcher watcher; 332 // THROW_IF_FAILED(watcher.create(folder, true, wil::allChangeEvents, []() 333 // { 334 // // respond 335 // })); 336 337 enum class FolderChangeEvent : DWORD 338 { 339 ChangesLost = 0, // requies special handling, reset state as events were lost 340 Added = FILE_ACTION_ADDED, 341 Removed = FILE_ACTION_REMOVED, 342 Modified = FILE_ACTION_MODIFIED, 343 RenameOldName = FILE_ACTION_RENAMED_OLD_NAME, 344 RenameNewName = FILE_ACTION_RENAMED_NEW_NAME, 345 }; 346 347 enum class FolderChangeEvents : DWORD 348 { 349 None = 0, 350 FileName = FILE_NOTIFY_CHANGE_FILE_NAME, 351 DirectoryName = FILE_NOTIFY_CHANGE_DIR_NAME, 352 Attributes = FILE_NOTIFY_CHANGE_ATTRIBUTES, 353 FileSize = FILE_NOTIFY_CHANGE_SIZE, 354 LastWriteTime = FILE_NOTIFY_CHANGE_LAST_WRITE, 355 Security = FILE_NOTIFY_CHANGE_SECURITY, 356 All = FILE_NOTIFY_CHANGE_FILE_NAME | 357 FILE_NOTIFY_CHANGE_DIR_NAME | 358 FILE_NOTIFY_CHANGE_ATTRIBUTES | 359 FILE_NOTIFY_CHANGE_SIZE | 360 FILE_NOTIFY_CHANGE_LAST_WRITE | 361 FILE_NOTIFY_CHANGE_SECURITY 362 }; 363 DEFINE_ENUM_FLAG_OPERATORS(FolderChangeEvents); 364 365 /// @cond 366 namespace details 367 { 368 struct folder_watcher_state 369 { folder_watcher_statefolder_watcher_state370 folder_watcher_state(wistd::function<void()> &&callback) : m_callback(wistd::move(callback)) 371 { 372 } 373 wistd::function<void()> m_callback; 374 // Order is important, need to close the thread pool wait before the change handle. 375 unique_hfind_change m_findChangeHandle; 376 unique_threadpool_wait m_threadPoolWait; 377 }; 378 delete_folder_watcher_state(_In_opt_ folder_watcher_state * storage)379 inline void delete_folder_watcher_state(_In_opt_ folder_watcher_state *storage) { delete storage; } 380 381 typedef resource_policy<folder_watcher_state *, decltype(&details::delete_folder_watcher_state), 382 details::delete_folder_watcher_state, details::pointer_access_none> folder_watcher_state_resource_policy; 383 } 384 /// @endcond 385 386 template <typename storage_t, typename err_policy = err_exception_policy> 387 class folder_watcher_t : public storage_t 388 { 389 public: 390 // forward all base class constructors... 391 template <typename... args_t> folder_watcher_t(args_t &&...args)392 explicit folder_watcher_t(args_t&&... args) WI_NOEXCEPT : storage_t(wistd::forward<args_t>(args)...) {} 393 394 // HRESULT or void error handling... 395 typedef typename err_policy::result result; 396 397 // Exception-based constructors folder_watcher_t(PCWSTR folderToWatch,bool isRecursive,FolderChangeEvents filter,wistd::function<void ()> && callback)398 folder_watcher_t(PCWSTR folderToWatch, bool isRecursive, FolderChangeEvents filter, wistd::function<void()> &&callback) 399 { 400 static_assert(wistd::is_same<void, result>::value, "this constructor requires exceptions; use the create method"); 401 create(folderToWatch, isRecursive, filter, wistd::move(callback)); 402 } 403 create(PCWSTR folderToWatch,bool isRecursive,FolderChangeEvents filter,wistd::function<void ()> && callback)404 result create(PCWSTR folderToWatch, bool isRecursive, FolderChangeEvents filter, wistd::function<void()> &&callback) 405 { 406 return err_policy::HResult(create_common(folderToWatch, isRecursive, filter, wistd::move(callback))); 407 } 408 private: 409 // Factored into a standalone function to support Clang which does not support conversion of stateless lambdas 410 // to __stdcall callback(PTP_CALLBACK_INSTANCE,void * context,TP_WAIT * pThreadPoolWait,TP_WAIT_RESULT)411 static void __stdcall callback(PTP_CALLBACK_INSTANCE /*Instance*/, void *context, TP_WAIT *pThreadPoolWait, TP_WAIT_RESULT /*result*/) 412 { 413 auto watcherState = static_cast<details::folder_watcher_state *>(context); 414 watcherState->m_callback(); 415 416 // Rearm the wait. Should not fail with valid parameters. 417 FindNextChangeNotification(watcherState->m_findChangeHandle.get()); 418 SetThreadpoolWait(pThreadPoolWait, watcherState->m_findChangeHandle.get(), __nullptr); 419 } 420 421 // This function exists to avoid template expansion of this code based on err_policy. create_common(PCWSTR folderToWatch,bool isRecursive,FolderChangeEvents filter,wistd::function<void ()> && callback)422 HRESULT create_common(PCWSTR folderToWatch, bool isRecursive, FolderChangeEvents filter, wistd::function<void()> &&callback) 423 { 424 wistd::unique_ptr<details::folder_watcher_state> watcherState(new(std::nothrow) details::folder_watcher_state(wistd::move(callback))); 425 RETURN_IF_NULL_ALLOC(watcherState); 426 427 watcherState->m_findChangeHandle.reset(FindFirstChangeNotificationW(folderToWatch, isRecursive, static_cast<DWORD>(filter))); 428 RETURN_LAST_ERROR_IF(!watcherState->m_findChangeHandle); 429 430 watcherState->m_threadPoolWait.reset(CreateThreadpoolWait(&folder_watcher_t::callback, watcherState.get(), __nullptr)); 431 RETURN_LAST_ERROR_IF(!watcherState->m_threadPoolWait); 432 this->reset(watcherState.release()); // no more failures after this, pass ownership 433 SetThreadpoolWait(this->get()->m_threadPoolWait.get(), this->get()->m_findChangeHandle.get(), __nullptr); 434 return S_OK; 435 } 436 }; 437 438 typedef unique_any_t<folder_watcher_t<details::unique_storage<details::folder_watcher_state_resource_policy>, err_returncode_policy>> unique_folder_watcher_nothrow; 439 make_folder_watcher_nothrow(PCWSTR folderToWatch,bool isRecursive,FolderChangeEvents filter,wistd::function<void ()> && callback)440 inline unique_folder_watcher_nothrow make_folder_watcher_nothrow(PCWSTR folderToWatch, bool isRecursive, FolderChangeEvents filter, wistd::function<void()> &&callback) WI_NOEXCEPT 441 { 442 unique_folder_watcher_nothrow watcher; 443 watcher.create(folderToWatch, isRecursive, filter, wistd::move(callback)); 444 return watcher; // caller must test for success using if (watcher) 445 } 446 447 #ifdef WIL_ENABLE_EXCEPTIONS 448 typedef unique_any_t<folder_watcher_t<details::unique_storage<details::folder_watcher_state_resource_policy>, err_exception_policy>> unique_folder_watcher; 449 make_folder_watcher(PCWSTR folderToWatch,bool isRecursive,FolderChangeEvents filter,wistd::function<void ()> && callback)450 inline unique_folder_watcher make_folder_watcher(PCWSTR folderToWatch, bool isRecursive, FolderChangeEvents filter, wistd::function<void()> &&callback) 451 { 452 return unique_folder_watcher(folderToWatch, isRecursive, filter, wistd::move(callback)); 453 } 454 #endif // WIL_ENABLE_EXCEPTIONS 455 456 #pragma endregion 457 458 #pragma region Folder Reader 459 460 // Example use for throwing: 461 // auto reader = wil::make_folder_change_reader(folder.Path().c_str(), true, wil::FolderChangeEvents::All, 462 // [](wil::FolderChangeEvent event, PCWSTR fileName) 463 // { 464 // switch (event) 465 // { 466 // case wil::FolderChangeEvent::ChangesLost: break; 467 // case wil::FolderChangeEvent::Added: break; 468 // case wil::FolderChangeEvent::Removed: break; 469 // case wil::FolderChangeEvent::Modified: break; 470 // case wil::FolderChangeEvent::RenamedOldName: break; 471 // case wil::FolderChangeEvent::RenamedNewName: break; 472 // }); 473 // 474 // Example use for non throwing: 475 // wil::unique_folder_change_reader_nothrow reader; 476 // THROW_IF_FAILED(reader.create(folder, true, wil::FolderChangeEvents::All, 477 // [](wil::FolderChangeEvent event, PCWSTR fileName) 478 // { 479 // // handle changes 480 // })); 481 // 482 483 // @cond 484 namespace details 485 { 486 struct folder_change_reader_state 487 { folder_change_reader_statefolder_change_reader_state488 folder_change_reader_state(bool isRecursive, FolderChangeEvents filter, wistd::function<void(FolderChangeEvent, PCWSTR)> &&callback) 489 : m_callback(wistd::move(callback)), m_isRecursive(isRecursive), m_filter(filter) 490 { 491 } 492 ~folder_change_reader_statefolder_change_reader_state493 ~folder_change_reader_state() 494 { 495 if (m_tpIo != __nullptr) 496 { 497 TP_IO *tpIo = m_tpIo; 498 499 // Indicate to the callback function that this object is being torn 500 // down. 501 502 { 503 auto autoLock = m_cancelLock.lock_exclusive(); 504 m_tpIo = __nullptr; 505 } 506 507 // Cancel IO to terminate the file system monitoring operation. 508 509 if (m_folderHandle) 510 { 511 CancelIoEx(m_folderHandle.get(), &m_overlapped); 512 } 513 514 // Wait for callbacks to complete. 515 // 516 // N.B. This is a blocking call and must not be made within a 517 // callback or within a lock which is taken inside the 518 // callback. 519 520 WaitForThreadpoolIoCallbacks(tpIo, TRUE); 521 CloseThreadpoolIo(tpIo); 522 } 523 } 524 StartIofolder_change_reader_state525 HRESULT StartIo() 526 { 527 // Unfortunately we have to handle ref-counting of IOs on behalf of the 528 // thread pool. 529 StartThreadpoolIo(m_tpIo); 530 HRESULT hr = ReadDirectoryChangesW(m_folderHandle.get(), m_readBuffer, sizeof(m_readBuffer), 531 m_isRecursive, static_cast<DWORD>(m_filter), __nullptr, &m_overlapped, __nullptr) ? 532 S_OK : HRESULT_FROM_WIN32(::GetLastError()); 533 if (FAILED(hr)) 534 { 535 // This operation does not have the usual semantic of returning 536 // ERROR_IO_PENDING. 537 // WI_ASSERT(hr != HRESULT_FROM_WIN32(ERROR_IO_PENDING)); 538 539 // If the operation failed for whatever reason, ensure the TP 540 // ref counts are accurate. 541 542 CancelThreadpoolIo(m_tpIo); 543 } 544 return hr; 545 } 546 547 // void (wil::FolderChangeEvent event, PCWSTR fileName) 548 wistd::function<void(FolderChangeEvent, PCWSTR)> m_callback; 549 unique_handle m_folderHandle; 550 BOOL m_isRecursive = FALSE; 551 FolderChangeEvents m_filter = FolderChangeEvents::None; 552 OVERLAPPED m_overlapped{}; 553 TP_IO *m_tpIo = __nullptr; 554 srwlock m_cancelLock; 555 char m_readBuffer[4096]; // Consider alternative buffer sizes. With 512 byte buffer i was not able to observe overflow. 556 }; 557 delete_folder_change_reader_state(_In_opt_ folder_change_reader_state * storage)558 inline void delete_folder_change_reader_state(_In_opt_ folder_change_reader_state *storage) { delete storage; } 559 560 typedef resource_policy<folder_change_reader_state *, decltype(&details::delete_folder_change_reader_state), 561 details::delete_folder_change_reader_state, details::pointer_access_none> folder_change_reader_state_resource_policy; 562 } 563 /// @endcond 564 565 template <typename storage_t, typename err_policy = err_exception_policy> 566 class folder_change_reader_t : public storage_t 567 { 568 public: 569 // forward all base class constructors... 570 template <typename... args_t> folder_change_reader_t(args_t &&...args)571 explicit folder_change_reader_t(args_t&&... args) WI_NOEXCEPT : storage_t(wistd::forward<args_t>(args)...) {} 572 573 // HRESULT or void error handling... 574 typedef typename err_policy::result result; 575 576 // Exception-based constructors folder_change_reader_t(PCWSTR folderToWatch,bool isRecursive,FolderChangeEvents filter,wistd::function<void (FolderChangeEvent,PCWSTR)> && callback)577 folder_change_reader_t(PCWSTR folderToWatch, bool isRecursive, FolderChangeEvents filter, wistd::function<void(FolderChangeEvent, PCWSTR)> &&callback) 578 { 579 static_assert(wistd::is_same<void, result>::value, "this constructor requires exceptions; use the create method"); 580 create(folderToWatch, isRecursive, filter, wistd::move(callback)); 581 } 582 create(PCWSTR folderToWatch,bool isRecursive,FolderChangeEvents filter,wistd::function<void (FolderChangeEvent,PCWSTR)> && callback)583 result create(PCWSTR folderToWatch, bool isRecursive, FolderChangeEvents filter, wistd::function<void(FolderChangeEvent, PCWSTR)> &&callback) 584 { 585 return err_policy::HResult(create_common(folderToWatch, isRecursive, filter, wistd::move(callback))); 586 } 587 folder_handle()588 wil::unique_hfile& folder_handle() { return this->get()->m_folderHandle; } 589 590 private: 591 // Factored into a standalone function to support Clang which does not support conversion of stateless lambdas 592 // to __stdcall callback(PTP_CALLBACK_INSTANCE,void * context,void *,ULONG result,ULONG_PTR,TP_IO *)593 static void __stdcall callback(PTP_CALLBACK_INSTANCE /* Instance */, void *context, void * /*overlapped*/, 594 ULONG result, ULONG_PTR /* BytesTransferred */, TP_IO * /* Io */) 595 { 596 auto readerState = static_cast<details::folder_change_reader_state *>(context); 597 // WI_ASSERT(overlapped == &readerState->m_overlapped); 598 599 bool requeue = true; 600 if (result == ERROR_SUCCESS) 601 { 602 for (auto const& info : create_next_entry_offset_iterator(reinterpret_cast<FILE_NOTIFY_INFORMATION *>(readerState->m_readBuffer))) 603 { 604 wchar_t realtiveFileName[MAX_PATH]; 605 StringCchCopyNW(realtiveFileName, ARRAYSIZE(realtiveFileName), info.FileName, info.FileNameLength / sizeof(info.FileName[0])); 606 607 readerState->m_callback(static_cast<FolderChangeEvent>(info.Action), realtiveFileName); 608 } 609 } 610 else if (result == ERROR_NOTIFY_ENUM_DIR) 611 { 612 readerState->m_callback(FolderChangeEvent::ChangesLost, __nullptr); 613 } 614 else 615 { 616 requeue = false; 617 } 618 619 if (requeue) 620 { 621 // If the lock is held non-shared or the TP IO is nullptr, this 622 // structure is being torn down. Otherwise, monitor for further 623 // changes. 624 auto autoLock = readerState->m_cancelLock.try_lock_shared(); 625 if (autoLock && readerState->m_tpIo) 626 { 627 readerState->StartIo(); // ignoring failure here 628 } 629 } 630 } 631 632 // This function exists to avoid template expansion of this code based on err_policy. create_common(PCWSTR folderToWatch,bool isRecursive,FolderChangeEvents filter,wistd::function<void (FolderChangeEvent,PCWSTR)> && callback)633 HRESULT create_common(PCWSTR folderToWatch, bool isRecursive, FolderChangeEvents filter, wistd::function<void(FolderChangeEvent, PCWSTR)> &&callback) 634 { 635 wistd::unique_ptr<details::folder_change_reader_state> readerState(new(std::nothrow) details::folder_change_reader_state( 636 isRecursive, filter, wistd::move(callback))); 637 RETURN_IF_NULL_ALLOC(readerState); 638 639 readerState->m_folderHandle.reset(CreateFileW(folderToWatch, 640 FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE, 641 __nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, __nullptr)); 642 RETURN_LAST_ERROR_IF(!readerState->m_folderHandle); 643 644 readerState->m_tpIo = CreateThreadpoolIo(readerState->m_folderHandle.get(), &folder_change_reader_t::callback, readerState.get(), __nullptr); 645 RETURN_LAST_ERROR_IF_NULL(readerState->m_tpIo); 646 RETURN_IF_FAILED(readerState->StartIo()); 647 this->reset(readerState.release()); 648 return S_OK; 649 } 650 }; 651 652 typedef unique_any_t<folder_change_reader_t<details::unique_storage<details::folder_change_reader_state_resource_policy>, err_returncode_policy>> unique_folder_change_reader_nothrow; 653 make_folder_change_reader_nothrow(PCWSTR folderToWatch,bool isRecursive,FolderChangeEvents filter,wistd::function<void (FolderChangeEvent,PCWSTR)> && callback)654 inline unique_folder_change_reader_nothrow make_folder_change_reader_nothrow(PCWSTR folderToWatch, bool isRecursive, FolderChangeEvents filter, 655 wistd::function<void(FolderChangeEvent, PCWSTR)> &&callback) WI_NOEXCEPT 656 { 657 unique_folder_change_reader_nothrow watcher; 658 watcher.create(folderToWatch, isRecursive, filter, wistd::move(callback)); 659 return watcher; // caller must test for success using if (watcher) 660 } 661 662 #ifdef WIL_ENABLE_EXCEPTIONS 663 typedef unique_any_t<folder_change_reader_t<details::unique_storage<details::folder_change_reader_state_resource_policy>, err_exception_policy>> unique_folder_change_reader; 664 make_folder_change_reader(PCWSTR folderToWatch,bool isRecursive,FolderChangeEvents filter,wistd::function<void (FolderChangeEvent,PCWSTR)> && callback)665 inline unique_folder_change_reader make_folder_change_reader(PCWSTR folderToWatch, bool isRecursive, FolderChangeEvents filter, 666 wistd::function<void(FolderChangeEvent, PCWSTR)> &&callback) 667 { 668 return unique_folder_change_reader(folderToWatch, isRecursive, filter, wistd::move(callback)); 669 } 670 #endif // WIL_ENABLE_EXCEPTIONS 671 #pragma endregion 672 673 //! Dos and VolumeGuid paths are always extended length paths with the \\?\ prefix. 674 enum class VolumePrefix 675 { 676 Dos = VOLUME_NAME_DOS, // Extended Dos Device path form, e.g. \\?\C:\Users\Chris\AppData\Local\Temp\wil8C31.tmp 677 VolumeGuid = VOLUME_NAME_GUID, // \\?\Volume{588fb606-b95b-4eae-b3cb-1e49861aaf18}\Users\Chris\AppData\Local\Temp\wil8C31.tmp 678 // The following are special paths which can't be used with Win32 APIs, but are useful in other scenarios. 679 None = VOLUME_NAME_NONE, // Path without the volume root, e.g. \Users\Chris\AppData\Local\Temp\wil8C31.tmp 680 NtObjectName = VOLUME_NAME_NT, // Unique name used by Object Manager, e.g. \Device\HarddiskVolume4\Users\Chris\AppData\Local\Temp\wil8C31.tmp 681 }; 682 enum class PathOptions 683 { 684 Normalized = FILE_NAME_NORMALIZED, 685 Opened = FILE_NAME_OPENED, 686 }; 687 DEFINE_ENUM_FLAG_OPERATORS(PathOptions); 688 689 /** A strongly typed version of the Win32 API GetFinalPathNameByHandleW. 690 Get the full path name in different forms 691 Use this instead + VolumePrefix::None instead of GetFileInformationByHandleEx(FileNameInfo) to 692 get that path form. */ 693 template <typename string_type, size_t stackBufferLength = 256> 694 HRESULT GetFinalPathNameByHandleW(HANDLE fileHandle, string_type& path, 695 wil::VolumePrefix volumePrefix = wil::VolumePrefix::Dos, wil::PathOptions options = wil::PathOptions::Normalized) 696 { 697 return AdaptFixedSizeToAllocatedResult<string_type, stackBufferLength>(path, 698 [&](_Out_writes_(valueLength) PWSTR value, size_t valueLength, _Out_ size_t* valueLengthNeededWithNull) -> HRESULT 699 { 700 *valueLengthNeededWithNull = ::GetFinalPathNameByHandleW(fileHandle, value, static_cast<DWORD>(valueLength), 701 static_cast<DWORD>(volumePrefix) | static_cast<DWORD>(options)); 702 RETURN_LAST_ERROR_IF(*valueLengthNeededWithNull == 0); 703 WI_ASSERT((*value != L'\0') == (*valueLengthNeededWithNull < valueLength)); 704 if (*valueLengthNeededWithNull < valueLength) 705 { 706 (*valueLengthNeededWithNull)++; // it fit, account for the null 707 } 708 return S_OK; 709 }); 710 } 711 712 #ifdef WIL_ENABLE_EXCEPTIONS 713 /** A strongly typed version of the Win32 API GetFinalPathNameByHandleW. 714 Get the full path name in different forms. Use this + VolumePrefix::None 715 instead of GetFileInformationByHandleEx(FileNameInfo) to get that path form. */ 716 template <typename string_type = wil::unique_cotaskmem_string, size_t stackBufferLength = 256> 717 string_type GetFinalPathNameByHandleW(HANDLE fileHandle, 718 wil::VolumePrefix volumePrefix = wil::VolumePrefix::Dos, wil::PathOptions options = wil::PathOptions::Normalized) 719 { 720 string_type result; 721 THROW_IF_FAILED((GetFinalPathNameByHandleW<string_type, stackBufferLength>(fileHandle, result, volumePrefix, options))); 722 return result; 723 } 724 #endif 725 726 //! A strongly typed version of the Win32 API of GetCurrentDirectoryW. 727 //! Return a path in an allocated buffer for handling long paths. 728 template <typename string_type, size_t stackBufferLength = 256> GetCurrentDirectoryW(string_type & path)729 HRESULT GetCurrentDirectoryW(string_type& path) 730 { 731 return AdaptFixedSizeToAllocatedResult<string_type, stackBufferLength>(path, 732 [&](_Out_writes_(valueLength) PWSTR value, size_t valueLength, _Out_ size_t* valueLengthNeededWithNull) -> HRESULT 733 { 734 *valueLengthNeededWithNull = ::GetCurrentDirectoryW(static_cast<DWORD>(valueLength), value); 735 RETURN_LAST_ERROR_IF(*valueLengthNeededWithNull == 0); 736 WI_ASSERT((*value != L'\0') == (*valueLengthNeededWithNull < valueLength)); 737 if (*valueLengthNeededWithNull < valueLength) 738 { 739 (*valueLengthNeededWithNull)++; // it fit, account for the null 740 } 741 return S_OK; 742 }); 743 } 744 745 #ifdef WIL_ENABLE_EXCEPTIONS 746 //! A strongly typed version of the Win32 API of GetCurrentDirectoryW. 747 //! Return a path in an allocated buffer for handling long paths. 748 template <typename string_type = wil::unique_cotaskmem_string, size_t stackBufferLength = 256> GetCurrentDirectoryW()749 string_type GetCurrentDirectoryW() 750 { 751 string_type result; 752 THROW_IF_FAILED((GetCurrentDirectoryW<string_type, stackBufferLength>(result))); 753 return result; 754 } 755 #endif 756 757 // TODO: add support for these and other similar APIs. 758 // GetShortPathNameW() 759 // GetLongPathNameW() 760 // GetWindowsDirectory() 761 // GetTempDirectory() 762 763 /// @cond 764 namespace details 765 { 766 template <FILE_INFO_BY_HANDLE_CLASS infoClass> struct MapInfoClassToInfoStruct; // failure to map is a usage error caught by the compiler 767 #define MAP_INFOCLASS_TO_STRUCT(InfoClass, InfoStruct, IsFixed, Extra) \ 768 template <> struct MapInfoClassToInfoStruct<InfoClass> \ 769 { \ 770 typedef InfoStruct type; \ 771 static bool const isFixed = IsFixed; \ 772 static size_t const extraSize = Extra; \ 773 }; 774 775 MAP_INFOCLASS_TO_STRUCT(FileBasicInfo, FILE_BASIC_INFO, true, 0); 776 MAP_INFOCLASS_TO_STRUCT(FileStandardInfo, FILE_STANDARD_INFO, true, 0); 777 MAP_INFOCLASS_TO_STRUCT(FileNameInfo, FILE_NAME_INFO, false, 32); 778 MAP_INFOCLASS_TO_STRUCT(FileRenameInfo, FILE_RENAME_INFO, false, 32); 779 MAP_INFOCLASS_TO_STRUCT(FileDispositionInfo, FILE_DISPOSITION_INFO, true, 0); 780 MAP_INFOCLASS_TO_STRUCT(FileAllocationInfo, FILE_ALLOCATION_INFO, true, 0); 781 MAP_INFOCLASS_TO_STRUCT(FileEndOfFileInfo, FILE_END_OF_FILE_INFO, true, 0); 782 MAP_INFOCLASS_TO_STRUCT(FileStreamInfo, FILE_STREAM_INFO, false, 32); 783 MAP_INFOCLASS_TO_STRUCT(FileCompressionInfo, FILE_COMPRESSION_INFO, true, 0); 784 MAP_INFOCLASS_TO_STRUCT(FileAttributeTagInfo, FILE_ATTRIBUTE_TAG_INFO, true, 0); 785 MAP_INFOCLASS_TO_STRUCT(FileIdBothDirectoryInfo, FILE_ID_BOTH_DIR_INFO, false, 4096); 786 MAP_INFOCLASS_TO_STRUCT(FileIdBothDirectoryRestartInfo, FILE_ID_BOTH_DIR_INFO, true, 0); 787 MAP_INFOCLASS_TO_STRUCT(FileIoPriorityHintInfo, FILE_IO_PRIORITY_HINT_INFO, true, 0); 788 MAP_INFOCLASS_TO_STRUCT(FileRemoteProtocolInfo, FILE_REMOTE_PROTOCOL_INFO, true, 0); 789 MAP_INFOCLASS_TO_STRUCT(FileFullDirectoryInfo, FILE_FULL_DIR_INFO, false, 4096); 790 MAP_INFOCLASS_TO_STRUCT(FileFullDirectoryRestartInfo, FILE_FULL_DIR_INFO, true, 0); 791 #if (_WIN32_WINNT >= _WIN32_WINNT_WIN8) 792 MAP_INFOCLASS_TO_STRUCT(FileStorageInfo, FILE_STORAGE_INFO, true, 0); 793 MAP_INFOCLASS_TO_STRUCT(FileAlignmentInfo, FILE_ALIGNMENT_INFO, true, 0); 794 MAP_INFOCLASS_TO_STRUCT(FileIdInfo, FILE_ID_INFO, true, 0); 795 MAP_INFOCLASS_TO_STRUCT(FileIdExtdDirectoryInfo, FILE_ID_EXTD_DIR_INFO, false, 4096); 796 MAP_INFOCLASS_TO_STRUCT(FileIdExtdDirectoryRestartInfo, FILE_ID_EXTD_DIR_INFO, true, 0); 797 #endif 798 799 // Type unsafe version used in the implementation to avoid template bloat. GetFileInfo(HANDLE fileHandle,FILE_INFO_BY_HANDLE_CLASS infoClass,size_t allocationSize,_Outptr_result_nullonfailure_ void ** result)800 inline HRESULT GetFileInfo(HANDLE fileHandle, FILE_INFO_BY_HANDLE_CLASS infoClass, size_t allocationSize, 801 _Outptr_result_nullonfailure_ void **result) 802 { 803 *result = nullptr; 804 805 wistd::unique_ptr<char[]> resultHolder(new(std::nothrow) char[allocationSize]); 806 RETURN_IF_NULL_ALLOC(resultHolder); 807 808 for (;;) 809 { 810 if (GetFileInformationByHandleEx(fileHandle, infoClass, resultHolder.get(), static_cast<DWORD>(allocationSize))) 811 { 812 *result = resultHolder.release(); 813 break; 814 } 815 else 816 { 817 DWORD const lastError = ::GetLastError(); 818 if (lastError == ERROR_MORE_DATA) 819 { 820 allocationSize *= 2; 821 resultHolder.reset(new(std::nothrow) char[allocationSize]); 822 RETURN_IF_NULL_ALLOC(resultHolder); 823 } 824 else if (lastError == ERROR_NO_MORE_FILES) // for folder enumeration cases 825 { 826 break; 827 } 828 else if (lastError == ERROR_INVALID_PARAMETER) // operation not supported by file system 829 { 830 return HRESULT_FROM_WIN32(lastError); 831 } 832 else 833 { 834 RETURN_WIN32(lastError); 835 } 836 } 837 } 838 return S_OK; 839 } 840 } 841 /// @endcond 842 843 /** Get file information for a variable sized structure, returns an HRESULT. 844 ~~~ 845 wistd::unique_ptr<FILE_NAME_INFO> fileNameInfo; 846 RETURN_IF_FAILED(GetFileInfoNoThrow<FileNameInfo>(fileHandle, fileNameInfo)); 847 ~~~ 848 */ 849 template <FILE_INFO_BY_HANDLE_CLASS infoClass, typename wistd::enable_if<!details::MapInfoClassToInfoStruct<infoClass>::isFixed, int>::type = 0> GetFileInfoNoThrow(HANDLE fileHandle,wistd::unique_ptr<typename details::MapInfoClassToInfoStruct<infoClass>::type> & result)850 HRESULT GetFileInfoNoThrow(HANDLE fileHandle, wistd::unique_ptr<typename details::MapInfoClassToInfoStruct<infoClass>::type> &result) WI_NOEXCEPT 851 { 852 void *rawResult; 853 HRESULT hr = details::GetFileInfo(fileHandle, infoClass, 854 sizeof(typename details::MapInfoClassToInfoStruct<infoClass>::type) + details::MapInfoClassToInfoStruct<infoClass>::extraSize, 855 &rawResult); 856 result.reset(static_cast<typename details::MapInfoClassToInfoStruct<infoClass>::type*>(rawResult)); 857 RETURN_HR_IF_EXPECTED(hr, hr == E_INVALIDARG); // operation not supported by file system 858 RETURN_IF_FAILED(hr); 859 return S_OK; 860 } 861 862 /** Get file information for a fixed sized structure, returns an HRESULT. 863 ~~~ 864 FILE_BASIC_INFO fileBasicInfo; 865 RETURN_IF_FAILED(GetFileInfoNoThrow<FileBasicInfo>(fileHandle, &fileBasicInfo)); 866 ~~~ 867 */ 868 template <FILE_INFO_BY_HANDLE_CLASS infoClass, typename wistd::enable_if<details::MapInfoClassToInfoStruct<infoClass>::isFixed, int>::type = 0> GetFileInfoNoThrow(HANDLE fileHandle,_Out_ typename details::MapInfoClassToInfoStruct<infoClass>::type * result)869 HRESULT GetFileInfoNoThrow(HANDLE fileHandle, _Out_ typename details::MapInfoClassToInfoStruct<infoClass>::type *result) WI_NOEXCEPT 870 { 871 const HRESULT hr = GetFileInformationByHandleEx(fileHandle, infoClass, result, sizeof(*result)) ? 872 S_OK : HRESULT_FROM_WIN32(::GetLastError()); 873 RETURN_HR_IF_EXPECTED(hr, hr == E_INVALIDARG); // operation not supported by file system 874 RETURN_IF_FAILED(hr); 875 return S_OK; 876 } 877 878 #ifdef _CPPUNWIND 879 /** Get file information for a fixed sized structure, throws on failure. 880 ~~~ 881 auto fileBasicInfo = GetFileInfo<FileBasicInfo>(fileHandle); 882 ~~~ 883 */ 884 template <FILE_INFO_BY_HANDLE_CLASS infoClass, typename wistd::enable_if<details::MapInfoClassToInfoStruct<infoClass>::isFixed, int>::type = 0> GetFileInfo(HANDLE fileHandle)885 typename details::MapInfoClassToInfoStruct<infoClass>::type GetFileInfo(HANDLE fileHandle) 886 { 887 typename details::MapInfoClassToInfoStruct<infoClass>::type result; 888 THROW_IF_FAILED(GetFileInfoNoThrow<infoClass>(fileHandle, &result)); 889 return result; 890 } 891 892 /** Get file information for a variable sized structure, throws on failure. 893 ~~~ 894 auto fileBasicInfo = GetFileInfo<FileNameInfo>(fileHandle); 895 ~~~ 896 */ 897 template <FILE_INFO_BY_HANDLE_CLASS infoClass, typename wistd::enable_if<!details::MapInfoClassToInfoStruct<infoClass>::isFixed, int>::type = 0> GetFileInfo(HANDLE fileHandle)898 wistd::unique_ptr<typename details::MapInfoClassToInfoStruct<infoClass>::type> GetFileInfo(HANDLE fileHandle) 899 { 900 wistd::unique_ptr<typename details::MapInfoClassToInfoStruct<infoClass>::type> result; 901 THROW_IF_FAILED(GetFileInfoNoThrow<infoClass>(fileHandle, result)); 902 return result; 903 } 904 #endif // _CPPUNWIND 905 #endif // WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) 906 } 907 908 #endif // __WIL_FILESYSTEM_INCLUDED 909