Skip to content

gh-113191: Add support of os.fchmod() on Windows #113192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Dec 24, 2023
Merged
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
8 changes: 6 additions & 2 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1001,11 +1001,14 @@ as internal buffering of data.

.. audit-event:: os.chmod path,mode,dir_fd os.fchmod

.. availability:: Unix.
.. availability:: Unix, Windows.

The function is limited on Emscripten and WASI, see
:ref:`wasm-availability` for more information.

.. versionchanged:: 3.13
Added support on Windows.


.. function:: fchown(fd, uid, gid)

Expand Down Expand Up @@ -2077,7 +2080,8 @@ features:
Accepts a :term:`path-like object`.

.. versionchanged:: 3.13
Added support for the *follow_symlinks* argument on Windows.
Added support for a file descriptor and the *follow_symlinks* argument
on Windows.


.. function:: chown(path, uid, gid, *, dir_fd=None, follow_symlinks=True)
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@ os
``False`` on Windows.
(Contributed by Serhiy Storchaka in :gh:`59616`)

* Add support of :func:`os.fchmod` and a file descriptor
in :func:`os.chmod` on Windows.
(Contributed by Serhiy Storchaka in :gh:`113191`)

* :func:`os.posix_spawn` now accepts ``env=None``, which makes the newly spawned
process use the current process environment.
(Contributed by Jakub Kulik in :gh:`113119`.)
Expand Down
1 change: 1 addition & 0 deletions Lib/os.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def _add(str, fn):
_set = set()
_add("HAVE_FCHDIR", "chdir")
_add("HAVE_FCHMOD", "chmod")
_add("MS_WINDOWS", "chmod")
_add("HAVE_FCHOWN", "chown")
_add("HAVE_FDOPENDIR", "listdir")
_add("HAVE_FDOPENDIR", "scandir")
Expand Down
13 changes: 10 additions & 3 deletions Lib/test/test_posix.py
Original file line number Diff line number Diff line change
Expand Up @@ -936,25 +936,26 @@ def test_utime(self):
posix.utime(os_helper.TESTFN, (now, now))

def check_chmod(self, chmod_func, target, **kwargs):
closefd = not isinstance(target, int)
mode = os.stat(target).st_mode
try:
new_mode = mode & ~(stat.S_IWOTH | stat.S_IWGRP | stat.S_IWUSR)
chmod_func(target, new_mode, **kwargs)
self.assertEqual(os.stat(target).st_mode, new_mode)
if stat.S_ISREG(mode):
try:
with open(target, 'wb+'):
with open(target, 'wb+', closefd=closefd):
pass
except PermissionError:
pass
new_mode = mode | (stat.S_IWOTH | stat.S_IWGRP | stat.S_IWUSR)
chmod_func(target, new_mode, **kwargs)
self.assertEqual(os.stat(target).st_mode, new_mode)
if stat.S_ISREG(mode):
with open(target, 'wb+'):
with open(target, 'wb+', closefd=closefd):
pass
finally:
posix.chmod(target, mode)
chmod_func(target, mode)

@os_helper.skip_unless_working_chmod
def test_chmod_file(self):
Expand All @@ -971,6 +972,12 @@ def test_chmod_dir(self):
target = self.tempdir()
self.check_chmod(posix.chmod, target)

@os_helper.skip_unless_working_chmod
def test_fchmod_file(self):
with open(os_helper.TESTFN, 'wb+') as f:
self.check_chmod(posix.fchmod, f.fileno())
self.check_chmod(posix.chmod, f.fileno())

@unittest.skipUnless(hasattr(posix, 'lchmod'), 'test needs os.lchmod()')
def test_lchmod_file(self):
self.check_chmod(posix.lchmod, os_helper.TESTFN)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add support of :func:`os.fchmod` and a file descriptor in :func:`os.chmod`
on Windows.
6 changes: 3 additions & 3 deletions Modules/clinic/posixmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 56 additions & 24 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2855,6 +2855,8 @@ FTRUNCATE
#ifdef MS_WINDOWS
#undef PATH_HAVE_FTRUNCATE
#define PATH_HAVE_FTRUNCATE 1
#undef PATH_HAVE_FCHMOD
#define PATH_HAVE_FCHMOD 1
#endif

/*[python input]
Expand Down Expand Up @@ -3332,7 +3334,38 @@ win32_lchmod(LPCWSTR path, int mode)
}
return SetFileAttributesW(path, attr);
}
#endif

static int
win32_hchmod(HANDLE hfile, int mode)
{
FILE_BASIC_INFO info;
if (!GetFileInformationByHandleEx(hfile, FileBasicInfo,
&info, sizeof(info)))
{
return 0;
}
if (mode & _S_IWRITE) {
info.FileAttributes &= ~FILE_ATTRIBUTE_READONLY;
}
else {
info.FileAttributes |= FILE_ATTRIBUTE_READONLY;
}
return SetFileInformationByHandle(hfile, FileBasicInfo,
&info, sizeof(info));
}

static int
win32_fchmod(int fd, int mode)
{
HANDLE hfile = _Py_get_osfhandle_noraise(fd);
if (hfile == INVALID_HANDLE_VALUE) {
SetLastError(ERROR_INVALID_HANDLE);
return 0;
}
return win32_hchmod(hfile, mode);
}

#endif /* MS_WINDOWS */

/*[clinic input]
os.chmod
Expand Down Expand Up @@ -3395,27 +3428,16 @@ os_chmod_impl(PyObject *module, path_t *path, int mode, int dir_fd,
#ifdef MS_WINDOWS
result = 0;
Py_BEGIN_ALLOW_THREADS
if (follow_symlinks) {
HANDLE hfile;
FILE_BASIC_INFO info;

hfile = CreateFileW(path->wide,
FILE_READ_ATTRIBUTES|FILE_WRITE_ATTRIBUTES,
0, NULL,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (path->fd != -1) {
result = win32_fchmod(path->fd, mode);
}
else if (follow_symlinks) {
HANDLE hfile = CreateFileW(path->wide,
FILE_READ_ATTRIBUTES|FILE_WRITE_ATTRIBUTES,
0, NULL,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (hfile != INVALID_HANDLE_VALUE) {
if (GetFileInformationByHandleEx(hfile, FileBasicInfo,
&info, sizeof(info)))
{
if (mode & _S_IWRITE) {
info.FileAttributes &= ~FILE_ATTRIBUTE_READONLY;
}
else {
info.FileAttributes |= FILE_ATTRIBUTE_READONLY;
}
result = SetFileInformationByHandle(hfile, FileBasicInfo,
&info, sizeof(info));
}
result = win32_hchmod(hfile, mode);
(void)CloseHandle(hfile);
}
}
Expand Down Expand Up @@ -3511,7 +3533,7 @@ os_chmod_impl(PyObject *module, path_t *path, int mode, int dir_fd,
}


#ifdef HAVE_FCHMOD
#if defined(HAVE_FCHMOD) || defined(MS_WINDOWS)
/*[clinic input]
os.fchmod

Expand All @@ -3533,23 +3555,33 @@ os_fchmod_impl(PyObject *module, int fd, int mode)
/*[clinic end generated code: output=afd9bc05b4e426b3 input=b5594618bbbc22df]*/
{
int res;
int async_err = 0;

if (PySys_Audit("os.chmod", "iii", fd, mode, -1) < 0) {
return NULL;
}

#ifdef MS_WINDOWS
res = 0;
Py_BEGIN_ALLOW_THREADS
res = win32_fchmod(fd, mode);
Py_END_ALLOW_THREADS
if (!res) {
return PyErr_SetFromWindowsErr(0);
}
#else /* MS_WINDOWS */
int async_err = 0;
do {
Py_BEGIN_ALLOW_THREADS
res = fchmod(fd, mode);
Py_END_ALLOW_THREADS
} while (res != 0 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
if (res != 0)
return (!async_err) ? posix_error() : NULL;
#endif /* MS_WINDOWS */

Py_RETURN_NONE;
}
#endif /* HAVE_FCHMOD */
#endif /* HAVE_FCHMOD || MS_WINDOWS */


#if defined(HAVE_LCHMOD) || defined(MS_WINDOWS)
Expand Down