Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
53 changes: 53 additions & 0 deletions Lib/test/test_os/test_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,59 @@ def test_unlink_removes_junction(self):
self.assertFalse(os.path.exists(self.junction))


class Win32ChmodTests(unittest.TestCase):
# gh-140774: os.chmod(follow_symlinks=True) and os.fchmod() go through
# SetFileInformationByHandle(), which interprets FileAttributes == 0 as
# "leave the attributes unchanged". Clearing the read-only flag of a
# file with no other attribute set must substitute FILE_ATTRIBUTE_NORMAL
# for 0 to take effect.

def setUp(self):
self.fname = os.path.abspath(os_helper.TESTFN)
self.addCleanup(os_helper.unlink, self.fname)
self.addCleanup(self._make_writable)
create_file(self.fname)

def _make_writable(self):
try:
os.chmod(self.fname, stat.S_IWRITE | stat.S_IREAD,
follow_symlinks=False)
except OSError:
pass

def _set_readonly_only_attribute(self):
# Make FILE_ATTRIBUTE_READONLY the only attribute of the file
# (in particular, clear FILE_ATTRIBUTE_ARCHIVE).
ctypes = import_helper.import_module('ctypes')
kernel32 = ctypes.WinDLL('Kernel32.dll', use_last_error=True)
if not kernel32.SetFileAttributesW(self.fname,
stat.FILE_ATTRIBUTE_READONLY):
raise ctypes.WinError(ctypes.get_last_error())
attributes = os.stat(self.fname).st_file_attributes
if attributes != stat.FILE_ATTRIBUTE_READONLY:
self.skipTest('file system reports extra file attributes '
f'{attributes:#x}')

def _assert_writable(self):
self.assertEqual(os.stat(self.fname).st_file_attributes
& stat.FILE_ATTRIBUTE_READONLY, 0)

def test_chmod_clears_readonly_without_other_attributes(self):
self._set_readonly_only_attribute()
os.chmod(self.fname, stat.S_IWRITE | stat.S_IREAD,
follow_symlinks=True)
self._assert_writable()

def test_fchmod_clears_readonly_without_other_attributes(self):
fd = os.open(self.fname, os.O_RDWR)
try:
self._set_readonly_only_attribute()
os.fchmod(fd, stat.S_IWRITE | stat.S_IREAD)
finally:
os.close(fd)
self._assert_writable()


class Win32NtTests(unittest.TestCase):
def test_getfinalpathname_handles(self):
nt = import_helper.import_module('nt')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :func:`os.chmod` with ``follow_symlinks=True``, :func:`os.fchmod` and
:meth:`pathlib.Path.chmod` on Windows: clearing the read-only attribute of a
file which has no other attribute set is no longer silently ignored.
6 changes: 6 additions & 0 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -4021,6 +4021,12 @@ win32_hchmod(HANDLE hfile, int mode)
}
if (mode & _S_IWRITE) {
info.FileAttributes &= ~FILE_ATTRIBUTE_READONLY;
if (info.FileAttributes == 0) {
/* SetFileInformationByHandle() interprets FileAttributes == 0
as "leave the attributes unchanged"; FILE_ATTRIBUTE_NORMAL
is the documented way to clear all attributes. */
info.FileAttributes = FILE_ATTRIBUTE_NORMAL;
}
}
else {
info.FileAttributes |= FILE_ATTRIBUTE_READONLY;
Expand Down
Loading