Skip to content

Commit b81d55a

Browse files
author
SparshGarg999
committed
fs: support removing read-only files in rmSync on Windows
On Windows, libc++ std::filesystem::remove and remove_all do not automatically clear the read-only attribute before deleting a file (unlike MSVC STL). This causes fs.rmSync to fail with EPERM when trying to remove read-only files in environments where Node.js is built using clang libc++ (such as Electron). This commit introduces a Windows-specific helper ClearReadOnlyAttributeW which clears the FILE_ATTRIBUTE_READONLY attribute recursively (or for a single file) when operation_not_permitted is returned, allowing rmSync to successfully delete read-only files/folders. Fixes: #64374 Signed-off-by: SparshGarg999 <sparshgarg999@gmail.com>
1 parent a4821c8 commit b81d55a

2 files changed

Lines changed: 81 additions & 0 deletions

File tree

src/node_file.cc

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1745,6 +1745,41 @@ static void RMDir(const FunctionCallbackInfo<Value>& args) {
17451745
}
17461746
}
17471747

1748+
#ifdef _WIN32
1749+
static void ClearReadOnlyAttributeWHelper(const wchar_t* path) {
1750+
DWORD attrs = GetFileAttributesW(path);
1751+
if (attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_READONLY)) {
1752+
SetFileAttributesW(path, attrs & ~FILE_ATTRIBUTE_READONLY);
1753+
}
1754+
}
1755+
1756+
static void ClearReadOnlyAttributeW(const std::filesystem::path& path,
1757+
bool recursive) {
1758+
std::error_code ec;
1759+
auto file_status = std::filesystem::symlink_status(path, ec);
1760+
if (ec) return;
1761+
1762+
if (recursive &&
1763+
file_status.type() == std::filesystem::file_type::directory) {
1764+
for (const auto& entry : std::filesystem::recursive_directory_iterator(
1765+
path,
1766+
std::filesystem::directory_options::skip_permission_denied,
1767+
ec)) {
1768+
std::error_code entry_ec;
1769+
auto entry_status = entry.symlink_status(entry_ec);
1770+
if (entry_ec) continue;
1771+
if (entry_status.type() != std::filesystem::file_type::symlink) {
1772+
ClearReadOnlyAttributeWHelper(entry.path().c_str());
1773+
}
1774+
}
1775+
}
1776+
1777+
if (file_status.type() != std::filesystem::file_type::symlink) {
1778+
ClearReadOnlyAttributeWHelper(path.c_str());
1779+
}
1780+
}
1781+
#endif
1782+
17481783
static void RmSync(const FunctionCallbackInfo<Value>& args) {
17491784
Environment* env = Environment::GetCurrent(args);
17501785
Isolate* isolate = env->isolate();
@@ -1789,6 +1824,9 @@ static void RmSync(const FunctionCallbackInfo<Value>& args) {
17891824
};
17901825

17911826
int i = 1;
1827+
#ifdef _WIN32
1828+
bool cleared_readonly = false;
1829+
#endif
17921830

17931831
while (maxRetries >= 0) {
17941832
if (recursive) {
@@ -1797,6 +1835,22 @@ static void RmSync(const FunctionCallbackInfo<Value>& args) {
17971835
std::filesystem::remove(file_path, error);
17981836
}
17991837

1838+
#ifdef _WIN32
1839+
// On Windows, libc++ does not clear the read-only attribute before
1840+
// removing a file (unlike MSVC STL which does). Attempt to clear it
1841+
// manually when we get EPERM (operation_not_permitted) so that read-only
1842+
// files can be deleted, matching the behavior of official Node.js builds.
1843+
if (error == std::errc::operation_not_permitted && !cleared_readonly) {
1844+
cleared_readonly = true;
1845+
ClearReadOnlyAttributeW(file_path, recursive);
1846+
if (recursive) {
1847+
std::filesystem::remove_all(file_path, error);
1848+
} else {
1849+
std::filesystem::remove(file_path, error);
1850+
}
1851+
}
1852+
#endif // _WIN32
1853+
18001854
if (!error || error == std::errc::no_such_file_or_directory) {
18011855
return;
18021856
} else if (!can_omit_error(error)) {

test/parallel/test-fs-rm.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,3 +631,30 @@ if (isGitPresent) {
631631
}
632632
}
633633
}
634+
635+
{
636+
// Test that rmSync can delete read-only files (and directories containing read-only files recursively)
637+
const dirname = nextDirPath();
638+
const filePath = path.join(dirname, 'readonly-file.txt');
639+
const recursiveDir = path.join(dirname, 'subdir');
640+
const recursiveFilePath = path.join(recursiveDir, 'readonly-nested.txt');
641+
642+
fs.mkdirSync(recursiveDir, { recursive: true });
643+
fs.writeFileSync(filePath, 'hello');
644+
fs.writeFileSync(recursiveFilePath, 'world');
645+
646+
// Make files read-only
647+
fs.chmodSync(filePath, 0o444);
648+
fs.chmodSync(recursiveFilePath, 0o444);
649+
650+
// rmSync without recursive option on a file
651+
fs.rmSync(filePath);
652+
assert.strictEqual(fs.existsSync(filePath), false);
653+
654+
// rmSync with recursive option on a directory containing a read-only file
655+
fs.rmSync(recursiveDir, { recursive: true });
656+
assert.strictEqual(fs.existsSync(recursiveDir), false);
657+
658+
// Clean up parent directory
659+
fs.rmSync(dirname, { recursive: true, force: true });
660+
}

0 commit comments

Comments
 (0)