Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion include/wil/filesystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,29 @@ inline HRESULT RemoveDirectoryRecursiveNoThrow(
}
else
{
RETURN_IF_WIN32_BOOL_FALSE(::RemoveDirectoryW(path.get()));
// Try a RemoveDirectory. Some errors may be recoverable.
if (!::RemoveDirectoryW(path.get()))
{
// Fail for anything other than ERROR_ACCESS_DENIED with option to RemoveReadOnly available
bool potentiallyFixableReadOnlyProblem =
WI_IsFlagSet(options, RemoveDirectoryOptions::RemoveReadOnly) && ::GetLastError() == ERROR_ACCESS_DENIED;
RETURN_LAST_ERROR_IF(!potentiallyFixableReadOnlyProblem);

// Fail if the directory does not have read-only set, likely just an ACL problem
DWORD directoryAttr = ::GetFileAttributesW(path.get());
RETURN_LAST_ERROR_IF(!WI_IsFlagSet(directoryAttr, FILE_ATTRIBUTE_READONLY));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably GetLastError won't contain failure because you just called GetFileAttributesW right before this (which probably succeeded). And even if it did fail, you probably want the original error from the call to RemoveDirectoryW


// Remove read-only flag, setting to NORMAL if completely empty
WI_ClearFlag(directoryAttr, FILE_ATTRIBUTE_READONLY);
if (directoryAttr == 0)
{
directoryAttr = FILE_ATTRIBUTE_NORMAL;
}
Comment on lines +395 to +398
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary? Presumably the attributes have FILE_ATTRIBUTE_DIRECTORY set at a minimum.


// Set the new attributes and try to delete the directory again, returning any failure
::SetFileAttributesW(path.get(), directoryAttr);
RETURN_IF_WIN32_BOOL_FALSE(::RemoveDirectoryW(path.get()));
}
}
}
return S_OK;
Expand Down
30 changes: 22 additions & 8 deletions tests/FileSystemTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ TEST_CASE("FileSystemTests::VerifyRemoveDirectoryRecursiveDoesNotTraverseWithout
subFolderHandle.reset();
}

TEST_CASE("FileSystemTests::VerifyRemoveDirectoryRecursiveCanDeleteReadOnlyFiles", "[filesystem]")
TEST_CASE("FileSystemTests::VerifyRemoveDirectoryRecursiveCanDeleteReadOnlyFilesAndDirectories", "[filesystem]")
{
auto CreateRelativePath = [](PCWSTR root, PCWSTR name) {
wil::unique_hlocal_string path;
Expand All @@ -140,6 +140,12 @@ TEST_CASE("FileSystemTests::VerifyRemoveDirectoryRecursiveCanDeleteReadOnlyFiles
REQUIRE(fileHandle);
};

auto CreateReadOnlyDirectory = [](PCWSTR path) {
REQUIRE(::CreateDirectoryW(path, nullptr));
DWORD directoryAttr = ::GetFileAttributesW(path);
REQUIRE(::SetFileAttributesW(path, directoryAttr | FILE_ATTRIBUTE_READONLY));
};

wil::unique_cotaskmem_string tempPath;
REQUIRE_SUCCEEDED(wil::ExpandEnvironmentStringsW(LR"(%TEMP%)", tempPath));
const auto basePath = CreateRelativePath(tempPath.get(), L"FileSystemTests");
Expand All @@ -152,25 +158,33 @@ TEST_CASE("FileSystemTests::VerifyRemoveDirectoryRecursiveCanDeleteReadOnlyFiles
// Create a reparse point and a target folder that shouldn't get deleted
auto folderToDelete = CreateRelativePath(basePath.get(), L"folderToDelete");
REQUIRE(::CreateDirectoryW(folderToDelete.get(), nullptr));

auto topLevelReadOnly = CreateRelativePath(folderToDelete.get(), L"topLevelReadOnly.txt");
CreateReadOnlyFile(topLevelReadOnly.get());

auto topLevelReadOnlyDirectory = CreateRelativePath(folderToDelete.get(), L"topLevelReadOnly");
CreateReadOnlyDirectory(topLevelReadOnlyDirectory.get());

auto topLevelReadOnlyFile = CreateRelativePath(folderToDelete.get(), L"topLevelReadOnly.txt");
CreateReadOnlyFile(topLevelReadOnlyFile.get());

auto subLevel = CreateRelativePath(folderToDelete.get(), L"subLevel");
REQUIRE(::CreateDirectoryW(subLevel.get(), nullptr));

auto subLevelReadOnly = CreateRelativePath(subLevel.get(), L"subLevelReadOnly.txt");
CreateReadOnlyFile(subLevelReadOnly.get());
auto subLevelReadOnlyDirectory = CreateRelativePath(subLevel.get(), L"subLevelReadOnly");
CreateReadOnlyDirectory(subLevelReadOnlyDirectory.get());

auto subLevelReadOnlyFile = CreateRelativePath(subLevel.get(), L"subLevelReadOnly.txt");
CreateReadOnlyFile(subLevelReadOnlyFile.get());

// Delete will fail without the RemoveReadOnlyFlag
REQUIRE_FAILED(wil::RemoveDirectoryRecursiveNoThrow(folderToDelete.get()));
REQUIRE_SUCCEEDED(wil::RemoveDirectoryRecursiveNoThrow(folderToDelete.get(), wil::RemoveDirectoryOptions::RemoveReadOnly));

// Verify all files have been deleted
REQUIRE_FALSE(FileExists(subLevelReadOnly.get()));
REQUIRE_FALSE(FileExists(subLevelReadOnlyFile.get()));
REQUIRE_FALSE(DirectoryExists(subLevelReadOnlyDirectory.get()));
REQUIRE_FALSE(DirectoryExists(subLevel.get()));

REQUIRE_FALSE(FileExists(topLevelReadOnly.get()));
REQUIRE_FALSE(FileExists(topLevelReadOnlyFile.get()));
REQUIRE_FALSE(DirectoryExists(topLevelReadOnlyDirectory.get()));
REQUIRE_FALSE(DirectoryExists(folderToDelete.get()));
}

Expand Down