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
38 changes: 38 additions & 0 deletions Lib/test/test_os/test_windows.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -608,5 +608,43 @@ def cleanup():
self.assertGreaterEqual(stat1.st_atime, stat2.st_atime)


class Win32StatExecutableTests(unittest.TestCase):
# gh-84419: Windows strips trailing dots and spaces from the last
# component of the path, so they should be ignored when guessing
# the execute permissions from the file extension.

SUFFIXES = ['', ' ', ' ', '.', '..', ' . .']

def check(self, ext, mask):
filename = os_helper.TESTFN + ext
create_file(filename)
try:
for suffix in self.SUFFIXES:
with self.subTest(suffix=suffix):
mode = os.stat(filename + suffix).st_mode
self.assertEqual(mode & 0o111, mask)
finally:
os_helper.unlink(filename)

def test_executable_extension(self):
for ext in '.exe', '.bat', '.cmd', '.com', '.EXE', '.Bat':
with self.subTest(ext=ext):
self.check(ext, 0o111)

def test_not_executable_extension(self):
for ext in '.txt', '.py', '.exe.txt', '':
with self.subTest(ext=ext):
self.check(ext, 0)

def test_extended_path(self):
# The \\?\ prefix disables normalization: trailing spaces and dots
# are part of the file name.
filename = os.path.abspath(os_helper.TESTFN + '.exe')
create_file(filename)
self.addCleanup(os_helper.unlink, filename)
self.assertEqual(os.stat('\\\\?\\' + filename).st_mode & 0o111, 0o111)
self.assertRaises(OSError, os.stat, '\\\\?\\' + filename + ' ')


if __name__ == "__main__":
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
Fix :func:`os.stat` on Windows: trailing dots and spaces, which are ignored
by the operating system, are no longer taken into account when the execute
permissions are guessed from the file extension.
34 changes: 22 additions & 12 deletions Modules/posixmodule.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -2035,6 +2035,14 @@ win32_wchdir(LPCWSTR path)
#define HAVE_STRUCT_STAT_ST_FILE_ATTRIBUTES 1
#define HAVE_STRUCT_STAT_ST_REPARSE_TAG 1

/* The \\?\ prefix disables the path normalization, in particular
stripping of trailing dots and spaces. */
static int
is_extended_path(const wchar_t *path)
{
return wcsncmp(path, L"\\\\?\\", 4) == 0;
}

static void
find_data_to_file_info(WIN32_FIND_DATAW *pFileData,
FILE_BASIC_INFO* basic_info,
Expand DownExpand Up@@ -2108,12 +2116,20 @@ update_st_mode_from_path(const wchar_t *path, DWORD attr,
GetSecurityInfo, OpenThreadToken/OpenProcessToken, and
AccessCheck to check for generic read, write, and execute
access. */
const wchar_t *fileExtension = wcsrchr(path, '.');
if (fileExtension) {
if (_wcsicmp(fileExtension, L".exe") == 0 ||
_wcsicmp(fileExtension, L".bat") == 0 ||
_wcsicmp(fileExtension, L".cmd") == 0 ||
_wcsicmp(fileExtension, L".com") == 0) {
size_t len = wcslen(path);
if (!is_extended_path(path)) {
/* Trailing dots and spaces are stripped from the last component
of the path. */
while (len > 0 && (path[len - 1] == L'.' || path[len - 1] == L' ')) {
len--;
}
}
if (len >= 4) {
const wchar_t *fileExtension = path + len - 4;
if (_wcsnicmp(fileExtension, L".exe", 4) == 0 ||
_wcsnicmp(fileExtension, L".bat", 4) == 0 ||
_wcsnicmp(fileExtension, L".cmd", 4) == 0 ||
_wcsnicmp(fileExtension, L".com", 4) == 0) {
result->st_mode |= 0111;
}
}
Expand DownExpand Up@@ -16711,12 +16727,6 @@ static PyType_Spec DirEntryType_spec = {

#ifdef MS_WINDOWS

static int
is_extended_path(const wchar_t *path)
{
return wcsncmp(path, L"\\\\?\\", 4) == 0;
}

static wchar_t *
join_path_filenameW(const wchar_t *path_wide, const wchar_t *filename,
int normalize)
Expand Down
Loading