Skip to content

Commit 0d117a1

Browse files
gh-59616: Support os.chmod(follow_symlinks=True) and os.lchmod() on Windows
1 parent 3531ea4 commit 0d117a1

File tree

6 files changed

+186
-27
lines changed

6 files changed

+186
-27
lines changed

Lib/os.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ def _add(str, fn):
171171
_add("HAVE_FSTATAT", "stat")
172172
_add("HAVE_LCHFLAGS", "chflags")
173173
_add("HAVE_LCHMOD", "chmod")
174+
_add("MS_WINDOWS", "chmod")
174175
if _exists("lchown"): # mac os x10.3
175176
_add("HAVE_LCHOWN", "chown")
176177
_add("HAVE_LINKAT", "link")

Lib/tempfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ def _dont_follow_symlinks(func, path, *args):
273273
# Pass follow_symlinks=False, unless not supported on this platform.
274274
if func in _os.supports_follow_symlinks:
275275
func(path, *args, follow_symlinks=False)
276-
elif _os.name == 'nt' or not _os.path.islink(path):
276+
elif not _os.path.islink(path):
277277
func(path, *args)
278278

279279
def _resetperms(path):

Lib/test/libregrtest/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,7 @@ def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
492492
self.fail_rerun)
493493

494494
def run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
495+
selected = ('test_posix', 'test_tempfile')
495496
os.makedirs(self.tmp_dir, exist_ok=True)
496497
work_dir = get_work_dir(self.tmp_dir)
497498

Lib/test/test_posix.py

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121

2222
try:
2323
import posix
24+
nt = None
2425
except ImportError:
25-
import nt as posix
26+
import nt
27+
posix = nt
2628

2729
try:
2830
import pwd
@@ -935,6 +937,116 @@ def test_utime(self):
935937
posix.utime(os_helper.TESTFN, (int(now), int(now)))
936938
posix.utime(os_helper.TESTFN, (now, now))
937939

940+
def check_chmod(self, chmod_func, target, **kwargs):
941+
mode = os.stat(target).st_mode
942+
try:
943+
chmod_func(target, mode & ~stat.S_IWRITE, **kwargs)
944+
self.assertEqual(os.stat(target).st_mode, mode & ~stat.S_IWRITE)
945+
if stat.S_ISREG(mode):
946+
try:
947+
with open(target, 'wb+'):
948+
pass
949+
except PermissionError:
950+
pass
951+
chmod_func(target, mode | stat.S_IWRITE, **kwargs)
952+
self.assertEqual(os.stat(target).st_mode, mode | stat.S_IWRITE)
953+
if stat.S_ISREG(mode):
954+
with open(target, 'wb+'):
955+
pass
956+
finally:
957+
posix.chmod(target, mode)
958+
959+
def test_chmod_file(self):
960+
self.check_chmod(posix.chmod, os_helper.TESTFN)
961+
962+
def tempdir(self):
963+
target = os_helper.TESTFN + 'd'
964+
posix.mkdir(target)
965+
self.addCleanup(posix.rmdir, target)
966+
return target
967+
968+
def test_chmod_dir(self):
969+
target = self.tempdir()
970+
self.check_chmod(posix.chmod, target)
971+
972+
@unittest.skipUnless(hasattr(posix, 'lchmod'), 'test needs os.lchmod()')
973+
def test_lchmod_file(self):
974+
self.check_chmod(posix.lchmod, os_helper.TESTFN)
975+
self.check_chmod(posix.chmod, os_helper.TESTFN, follow_symlinks=False)
976+
977+
@unittest.skipUnless(hasattr(posix, 'lchmod'), 'test needs os.lchmod()')
978+
def test_lchmod_dir(self):
979+
target = self.tempdir()
980+
self.check_chmod(posix.lchmod, target)
981+
self.check_chmod(posix.chmod, target, follow_symlinks=False)
982+
983+
def check_chmod_link(self, chmod_func, target, link, **kwargs):
984+
target_mode = os.stat(target).st_mode
985+
link_mode = os.lstat(link).st_mode
986+
try:
987+
chmod_func(link, target_mode & ~stat.S_IWRITE, **kwargs)
988+
self.assertEqual(os.stat(target).st_mode, target_mode & ~stat.S_IWRITE)
989+
self.assertEqual(os.lstat(link).st_mode, link_mode)
990+
chmod_func(link, target_mode | stat.S_IWRITE)
991+
self.assertEqual(os.stat(target).st_mode, target_mode | stat.S_IWRITE)
992+
self.assertEqual(os.lstat(link).st_mode, link_mode)
993+
finally:
994+
posix.chmod(target, target_mode)
995+
996+
def check_lchmod_link(self, chmod_func, target, link, **kwargs):
997+
target_mode = os.stat(target).st_mode
998+
link_mode = os.lstat(link).st_mode
999+
chmod_func(link, link_mode & ~stat.S_IWRITE, **kwargs)
1000+
self.assertEqual(os.stat(target).st_mode, target_mode)
1001+
self.assertEqual(os.lstat(link).st_mode, link_mode & ~stat.S_IWRITE)
1002+
chmod_func(link, link_mode | stat.S_IWRITE)
1003+
self.assertEqual(os.stat(target).st_mode, target_mode)
1004+
self.assertEqual(os.lstat(link).st_mode, link_mode | stat.S_IWRITE)
1005+
1006+
@os_helper.skip_unless_symlink
1007+
def test_chmod_file_symlink(self):
1008+
target = os_helper.TESTFN
1009+
link = os_helper.TESTFN + '-link'
1010+
os.symlink(target, link)
1011+
self.addCleanup(posix.unlink, link)
1012+
if os.name == 'nt':
1013+
self.check_lchmod_link(posix.chmod, target, link)
1014+
else:
1015+
self.check_chmod_link(posix.chmod, target, link)
1016+
self.check_chmod_link(posix.chmod, target, link, follow_symlinks=True)
1017+
1018+
@os_helper.skip_unless_symlink
1019+
def test_chmod_dir_symlink(self):
1020+
target = self.tempdir()
1021+
link = os_helper.TESTFN + '-link'
1022+
os.symlink(target, link, target_is_directory=True)
1023+
self.addCleanup(posix.unlink, link)
1024+
if os.name == 'nt':
1025+
self.check_lchmod_link(posix.chmod, target, link)
1026+
else:
1027+
self.check_chmod_link(posix.chmod, target, link)
1028+
self.check_chmod_link(posix.chmod, target, link, follow_symlinks=True)
1029+
1030+
@unittest.skipUnless(hasattr(posix, 'lchmod'), 'test needs os.lchmod()')
1031+
@os_helper.skip_unless_symlink
1032+
def test_lchmod_file_symlink(self):
1033+
target = os_helper.TESTFN
1034+
link = os_helper.TESTFN + '-link'
1035+
os.symlink(target, link)
1036+
self.addCleanup(posix.unlink, link)
1037+
self.check_lchmod_link(posix.chmod, target, link, follow_symlinks=False)
1038+
self.check_lchmod_link(posix.lchmod, target, link)
1039+
1040+
@unittest.skipUnless(hasattr(posix, 'lchmod'), 'test needs os.lchmod()')
1041+
@os_helper.skip_unless_symlink
1042+
def test_lchmod_dir_symlink(self):
1043+
target = self.tempdir()
1044+
link = os_helper.TESTFN + '-link'
1045+
os.symlink(target, link)
1046+
self.addCleanup(posix.unlink, link)
1047+
self.check_lchmod_link(posix.chmod, target, link, follow_symlinks=False)
1048+
self.check_lchmod_link(posix.lchmod, target, link)
1049+
9381050
def _test_chflags_regular_file(self, chflags_func, target_file, **kwargs):
9391051
st = os.stat(target_file)
9401052
self.assertTrue(hasattr(st, 'st_flags'))

Modules/clinic/posixmodule.c.h

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/posixmodule.c

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3309,6 +3309,27 @@ os_fchdir_impl(PyObject *module, int fd)
33093309
}
33103310
#endif /* HAVE_FCHDIR */
33113311

3312+
#ifdef MS_WINDOWS
3313+
# define CHMOD_DEFAULT_FOLLOW_SYMLINKS 0
3314+
#else
3315+
# define CHMOD_DEFAULT_FOLLOW_SYMLINKS 1
3316+
#endif
3317+
3318+
#ifdef MS_WINDOWS
3319+
static int
3320+
win32_lchmod(LPCWSTR path, int mode)
3321+
{
3322+
DWORD attr = GetFileAttributesW(path);
3323+
if (attr == INVALID_FILE_ATTRIBUTES) {
3324+
return 0;
3325+
}
3326+
if (mode & _S_IWRITE)
3327+
attr &= ~FILE_ATTRIBUTE_READONLY;
3328+
else
3329+
attr |= FILE_ATTRIBUTE_READONLY;
3330+
return SetFileAttributesW(path, attr);
3331+
}
3332+
#endif
33123333

33133334
/*[clinic input]
33143335
os.chmod
@@ -3331,7 +3352,7 @@ os.chmod
33313352
and path should be relative; path will then be relative to that
33323353
directory.
33333354
3334-
follow_symlinks: bool = True
3355+
follow_symlinks: bool(c_default="CHMOD_DEFAULT_FOLLOW_SYMLINKS") = _CHMOD_DEFAULT_FOLLOW_SYMLINKS
33353356
If False, and the last element of the path is a symbolic link,
33363357
chmod will modify the symbolic link itself instead of the file
33373358
the link points to.
@@ -3348,43 +3369,55 @@ dir_fd and follow_symlinks may not be implemented on your platform.
33483369
static PyObject *
33493370
os_chmod_impl(PyObject *module, path_t *path, int mode, int dir_fd,
33503371
int follow_symlinks)
3351-
/*[clinic end generated code: output=5cf6a94915cc7bff input=674a14bc998de09d]*/
3372+
/*[clinic end generated code: output=5cf6a94915cc7bff input=b28ffee79f567860]*/
33523373
{
33533374
int result;
33543375

3355-
#ifdef MS_WINDOWS
3356-
DWORD attr;
3357-
#endif
3358-
33593376
#ifdef HAVE_FCHMODAT
33603377
int fchmodat_nofollow_unsupported = 0;
33613378
int fchmodat_unsupported = 0;
33623379
#endif
33633380

3364-
#if !(defined(HAVE_FCHMODAT) || defined(HAVE_LCHMOD))
3381+
#if !(defined(HAVE_FCHMODAT) || defined(HAVE_LCHMOD) || defined(MS_WINDOWS))
33653382
if (follow_symlinks_specified("chmod", follow_symlinks))
33663383
return NULL;
33673384
#endif
33683385

3369-
if (PySys_Audit("os.chmod", "Oii", path->object, mode,
3370-
dir_fd == DEFAULT_DIR_FD ? -1 : dir_fd) < 0) {
3386+
if (PySys_Audit("os.chmod", "OiiO", path->object, mode,
3387+
dir_fd == DEFAULT_DIR_FD ? -1 : dir_fd,
3388+
follow_symlinks ? Py_True : Py_False) < 0) {
33713389
return NULL;
33723390
}
33733391

33743392
#ifdef MS_WINDOWS
3393+
result = 0;
33753394
Py_BEGIN_ALLOW_THREADS
3376-
attr = GetFileAttributesW(path->wide);
3377-
if (attr == INVALID_FILE_ATTRIBUTES)
3378-
result = 0;
3395+
if (follow_symlinks) {
3396+
HANDLE hfile;
3397+
FILE_BASIC_INFO info;
3398+
3399+
hfile = CreateFileW(path->wide,
3400+
FILE_READ_ATTRIBUTES|FILE_WRITE_ATTRIBUTES,
3401+
0, NULL,
3402+
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
3403+
if (hfile != INVALID_HANDLE_VALUE) {
3404+
if (GetFileInformationByHandleEx(hfile, FileBasicInfo,
3405+
&info, sizeof(info)))
3406+
{
3407+
if (mode & _S_IWRITE)
3408+
info.FileAttributes &= ~FILE_ATTRIBUTE_READONLY;
3409+
else
3410+
info.FileAttributes |= FILE_ATTRIBUTE_READONLY;
3411+
result = SetFileInformationByHandle(hfile, FileBasicInfo,
3412+
&info, sizeof(info));
3413+
}
3414+
CloseHandle(hfile);
3415+
}
3416+
}
33793417
else {
3380-
if (mode & _S_IWRITE)
3381-
attr &= ~FILE_ATTRIBUTE_READONLY;
3382-
else
3383-
attr |= FILE_ATTRIBUTE_READONLY;
3384-
result = SetFileAttributesW(path->wide, attr);
3418+
result = win32_lchmod(path->wide, mode);
33853419
}
33863420
Py_END_ALLOW_THREADS
3387-
33883421
if (!result) {
33893422
return path_error(path);
33903423
}
@@ -3514,7 +3547,7 @@ os_fchmod_impl(PyObject *module, int fd, int mode)
35143547
#endif /* HAVE_FCHMOD */
35153548

35163549

3517-
#ifdef HAVE_LCHMOD
3550+
#if defined(HAVE_LCHMOD) || defined(MS_WINDOWS)
35183551
/*[clinic input]
35193552
os.lchmod
35203553
@@ -3532,19 +3565,29 @@ os_lchmod_impl(PyObject *module, path_t *path, int mode)
35323565
/*[clinic end generated code: output=082344022b51a1d5 input=90c5663c7465d24f]*/
35333566
{
35343567
int res;
3535-
if (PySys_Audit("os.chmod", "Oii", path->object, mode, -1) < 0) {
3568+
if (PySys_Audit("os.chmod", "OiiO", path->object, mode, -1, Py_False) < 0) {
35363569
return NULL;
35373570
}
3571+
#ifdef MS_WINDOWS
3572+
Py_BEGIN_ALLOW_THREADS
3573+
res = win32_lchmod(path->wide, mode);
3574+
Py_END_ALLOW_THREADS
3575+
if (!res) {
3576+
path_error(path);
3577+
return NULL;
3578+
}
3579+
#else /* MS_WINDOWS */
35383580
Py_BEGIN_ALLOW_THREADS
35393581
res = lchmod(path->narrow, mode);
35403582
Py_END_ALLOW_THREADS
35413583
if (res < 0) {
35423584
path_error(path);
35433585
return NULL;
35443586
}
3587+
#endif /* MS_WINDOWS */
35453588
Py_RETURN_NONE;
35463589
}
3547-
#endif /* HAVE_LCHMOD */
3590+
#endif /* HAVE_LCHMOD || MS_WINDOWS */
35483591

35493592

35503593
#ifdef HAVE_CHFLAGS
@@ -16927,6 +16970,7 @@ all_ins(PyObject *m)
1692716970
if (PyModule_AddIntConstant(m, "_LOAD_LIBRARY_SEARCH_USER_DIRS", LOAD_LIBRARY_SEARCH_USER_DIRS)) return -1;
1692816971
if (PyModule_AddIntConstant(m, "_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR", LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR)) return -1;
1692916972
#endif
16973+
if (PyModule_Add(m, "_CHMOD_DEFAULT_FOLLOW_SYMLINKS", PyBool_FromLong(CHMOD_DEFAULT_FOLLOW_SYMLINKS))) return -1;
1693016974

1693116975
return 0;
1693216976
}

0 commit comments

Comments
 (0)