From 6f7e9fa051ff5ccba30bf47346b7605070c8a0a2 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 24 Nov 2022 15:36:06 +0000 Subject: [PATCH] gh-99726: Adds os.statx function and associated constants --- Doc/library/os.rst | 147 ++++++++- Include/internal/pycore_fileutils.h | 2 + Include/internal/pycore_fileutils_windows.h | 77 +++++ .../pycore_global_objects_fini_generated.h | 1 + Include/internal/pycore_global_strings.h | 1 + .../internal/pycore_runtime_init_generated.h | 1 + .../internal/pycore_unicodeobject_generated.h | 2 + Lib/genericpath.py | 24 +- Lib/ntpath.py | 6 +- Lib/os.py | 13 + Lib/stat.py | 18 + Lib/test/test_os.py | 4 + ...2-11-24-14-57-48.gh-issue-99726.eayGOV.rst | 1 + Modules/clinic/posixmodule.c.h | 129 +++++++- Modules/posixmodule.c | 307 +++++++++++++++++- PC/pyconfig.h | 3 + Python/fileutils.c | 59 +++- configure.ac | 2 +- pyconfig.h.in | 3 + 19 files changed, 755 insertions(+), 45 deletions(-) create mode 100644 Include/internal/pycore_fileutils_windows.h create mode 100644 Misc/NEWS.d/next/Library/2022-11-24-14-57-48.gh-issue-99726.eayGOV.rst diff --git a/Doc/library/os.rst b/Doc/library/os.rst index 775aa32df99a46..12fde074f4493c 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -2839,11 +2839,65 @@ features: ``follow_symlinks=False`` had been specified instead of raising an error. +.. function:: statx(path, mask, *, dir_fd=None, follow_symlinks=True, flags=0) + + Get selected fields of the status of a file or a file descriptor. Perform the + equivalent of a :c:func:`statx` system call on the given path. *path* may be + specified as either a string or bytes -- directly or indirectly through the + :class:`PathLike` interface -- or as an open file descriptor. Return a + :class:`stat_result` object. + + *mask* is a bitwise combination of the ``STATX_*`` attributes in the + :mod:`stat` module, indicating which fields the caller intends to use. Note + that the set of fields returned may differ from what's requested, if the + operating system or file system does not support the metadata, or if it can + provide additional fields with no extra effort. + + This function normally follows symlinks; to stat a symlink add the argument + ``follow_symlinks=False``. + + This function can support :ref:`specifying a file descriptor ` and + :ref:`not following symlinks `. + + On Windows, passing ``follow_symlinks=False`` will disable following all + name-surrogate reparse points, which includes symlinks and directory + junctions. Other types of reparse points that do not resemble links or that + the operating system is unable to follow will be opened directly. When + following a chain of multiple links, this may result in the original link + being returned instead of the non-link that prevented full traversal. To + obtain stat results for the final path in this case, use the + :func:`os.path.realpath` function to resolve the path name as far as + possible and call :func:`lstat` on the result. This does not apply to + dangling symlinks or junction points, which will raise the usual exceptions. + + .. index:: module: stat + + Example:: + + >>> import os, stat + >>> statinfo = os.statx('somefile.txt', stat.STATX_SIZE) + >>> statinfo + os.stat_result(st_mode=33188, st_ino=0, st_dev=0, + st_nlink=1, st_uid=0, st_gid=0, st_size=264, st_atime=0, + st_mtime=0, st_ctime=0) + >>> statinfo.stx_mask & stat.STATX_SIZE + 512 + >>> statinfo.st_size + 264 + + .. seealso:: + + :func:`stat`, :func:`fstat` and :func:`lstat` functions. + + .. versionadded:: 3.12 + Added ``statx`` function. + + .. class:: stat_result Object whose attributes correspond roughly to the members of the :c:type:`stat` structure. It is used for the result of :func:`os.stat`, - :func:`os.fstat` and :func:`os.lstat`. + :func:`os.fstat`, :func:`os.lstat` and :func:`os.statx`. Attributes: @@ -2851,6 +2905,9 @@ features: File mode: file type and file mode bits (permissions). + This field is set with the :data:`stat.STATX_TYPE` and/or + :data:`stat.STATX_MODE` flags. + .. attribute:: st_ino Platform dependent, but if non-zero, uniquely identifies the @@ -2861,70 +2918,111 @@ features: `_ on Windows + This field is set with :data:`stat.STATX_INO`. + .. attribute:: st_dev Identifier of the device on which this file resides. + On Windows, this field is set with :data:`stat.STATX_INO`. + .. attribute:: st_nlink Number of hard links. + This field is set with :data:`stat.STATX_NLINK`. + .. attribute:: st_uid User identifier of the file owner. + This field is set with :data:`stat.STATX_UID`. + .. attribute:: st_gid Group identifier of the file owner. + This field is set with :data:`stat.STATX_GID`. + .. attribute:: st_size Size of the file in bytes, if it is a regular file or a symbolic link. The size of a symbolic link is the length of the pathname it contains, without a terminating null byte. + This field is set with :data:`stat.STATX_SIZE`. + + .. attribute:: stx_mask + + Flags indicating which values were set. :func`os.statx` allows specifying + a mask, though the result may include more or less than requested. Other + ``stat`` functions set a default value representing the information they + return. + Timestamps: .. attribute:: st_atime Time of most recent access expressed in seconds. + This field is set with :data:`stat.STATX_ATIME`. + .. attribute:: st_mtime Time of most recent content modification expressed in seconds. + This field is set with :data:`stat.STATX_MTIME`. + .. attribute:: st_ctime Platform dependent: * the time of most recent metadata change on Unix, - * the time of creation on Windows, expressed in seconds. + * the time of creation on Windows, expressed in seconds, except + when :data:`stat.STATX_CTIME` is in :attr:`stx_mask`, in which + case this is the time of the most recent metadata change + + This field is set with :data:`stat.STATX_CTIME`. .. attribute:: st_atime_ns Time of most recent access expressed in nanoseconds as an integer. + This field is set with :data:`stat.STATX_ATIME`. + .. attribute:: st_mtime_ns Time of most recent content modification expressed in nanoseconds as an integer. + This field is set with :data:`stat.STATX_MTIME`. + .. attribute:: st_ctime_ns Platform dependent: * the time of most recent metadata change on Unix, * the time of creation on Windows, expressed in nanoseconds as an - integer. + integer, except when :data:`stat.STATX_CTIME` is in :attr:`stx_mask`, + in which case this is the time of the most recent metadata change + + This field is set with :data:`stat.STATX_CTIME`. + + .. attribute:: st_birthtime + + Time of file creation, if available. The attribute may not be present if + your operating system does not support the field. + + This field is set with :data:`stat.STATX_BTIME`. .. note:: The exact meaning and resolution of the :attr:`st_atime`, - :attr:`st_mtime`, and :attr:`st_ctime` attributes depend on the operating - system and the file system. For example, on Windows systems using the FAT - or FAT32 file systems, :attr:`st_mtime` has 2-second resolution, and - :attr:`st_atime` has only 1-day resolution. See your operating system - documentation for details. + :attr:`st_mtime`, :attr:`st_ctime` and :attr:`st_birthtime` attributes + depend on the operating system and the file system. For example, on + Windows systems using the FAT or FAT32 file systems, :attr:`st_mtime` has + 2-second resolution, and :attr:`st_atime` has only 1-day resolution. + See your operating system documentation for details. Similarly, although :attr:`st_atime_ns`, :attr:`st_mtime_ns`, and :attr:`st_ctime_ns` are always expressed in nanoseconds, many @@ -2943,11 +3041,15 @@ features: Number of 512-byte blocks allocated for file. This may be smaller than :attr:`st_size`/512 when the file has holes. + This field is set with :data:`stat.STATX_BLOCKS`. + .. attribute:: st_blksize "Preferred" blocksize for efficient file system I/O. Writing to a file in smaller chunks may cause an inefficient read-modify-rewrite. + This field is set with :data:`stat.STATX_BLOCKSIZE`. + .. attribute:: st_rdev Type of device if an inode device. @@ -2956,6 +3058,20 @@ features: User defined flags for file. + .. attribute:: stx_attributes + + Additional attribute flags (``STATX_ATTR_*`` values). + + This field is only set for calls using :func:`os.statx`. + + .. attribute:: stx_attributes_mask + + Attribute flags (``STATX_ATTR_*``) that were supported on the file system + containing the file. Flags not set in this mask are meaningless in + :attr:`stx_attributes`. + + This field is only set for calls using :func:`os.statx`. + On other Unix systems (such as FreeBSD), the following attributes may be available (but may be only filled out if root tries to use them): @@ -2963,10 +3079,6 @@ features: File generation number. - .. attribute:: st_birthtime - - Time of file creation. - On Solaris and derivatives, the following attributes may also be available: @@ -2998,12 +3110,16 @@ features: :c:func:`GetFileInformationByHandle`. See the ``FILE_ATTRIBUTE_*`` constants in the :mod:`stat` module. + This field is requested with :data:`stat.STATX_TYPE`. + .. attribute:: st_reparse_tag When :attr:`st_file_attributes` has the ``FILE_ATTRIBUTE_REPARSE_POINT`` set, this field contains the tag identifying the type of reparse point. See the ``IO_REPARSE_TAG_*`` constants in the :mod:`stat` module. + This field is requested with :data:`stat.STATX_TYPE`. + The standard module :mod:`stat` defines functions and constants that are useful for extracting information from a :c:type:`stat` structure. (On Windows, some items are filled with dummy values.) @@ -3039,6 +3155,13 @@ features: files as :const:`S_IFCHR`, :const:`S_IFIFO` or :const:`S_IFBLK` as appropriate. + .. versionchanged:: 3.12 + Added the :attr:`stx_mask` member along with :func:`statx`. + + .. versionchanged:: 3.12 + Added the :attr:`st_birthtime` member on Windows. + + .. function:: statvfs(path) Perform a :c:func:`statvfs` system call on the given path. The return value is diff --git a/Include/internal/pycore_fileutils.h b/Include/internal/pycore_fileutils.h index ac89c43d569c07..b9276531663a6a 100644 --- a/Include/internal/pycore_fileutils.h +++ b/Include/internal/pycore_fileutils.h @@ -82,6 +82,8 @@ struct _Py_stat_struct { int st_ctime_nsec; unsigned long st_file_attributes; unsigned long st_reparse_tag; + time_t st_btime; + int st_btime_nsec; }; #else # define _Py_stat_struct stat diff --git a/Include/internal/pycore_fileutils_windows.h b/Include/internal/pycore_fileutils_windows.h new file mode 100644 index 00000000000000..d1545eff30f51a --- /dev/null +++ b/Include/internal/pycore_fileutils_windows.h @@ -0,0 +1,77 @@ +#ifndef Py_INTERNAL_FILEUTILS_WINDOWS_H +#define Py_INTERNAL_FILEUTILS_WINDOWS_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "Py_BUILD_CORE must be defined to include this header" +#endif + +#ifdef MS_WINDOWS + +#if !defined(NTDDI_WIN10_NI) || !(NTDDI_VERSION >= NTDDI_WIN10_NI) +typedef struct _FILE_STAT_BASIC_INFORMATION { + LARGE_INTEGER FileId; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER AllocationSize; + LARGE_INTEGER EndOfFile; + ULONG FileAttributes; + ULONG ReparseTag; + ULONG NumberOfLinks; + ULONG DeviceType; + ULONG DeviceCharacteristics; +} FILE_STAT_BASIC_INFORMATION; + +typedef enum _FILE_INFO_BY_NAME_CLASS { + FileStatByNameInfo, + FileStatLxByNameInfo, + FileCaseSensitiveByNameInfo, + FileStatBasicByNameInfo, + MaximumFileInfoByNameClass +} FILE_INFO_BY_NAME_CLASS; +#endif + +typedef BOOL (WINAPI *PGetFileInformationByName)( + PCWSTR FileName, + FILE_INFO_BY_NAME_CLASS FileInformationClass, + PVOID FileInfoBuffer, + ULONG FileInfoBufferSize +); + +static inline BOOL GetFileInformationByName( + PCWSTR FileName, + FILE_INFO_BY_NAME_CLASS FileInformationClass, + PVOID FileInfoBuffer, + ULONG FileInfoBufferSize +) { + static PGetFileInformationByName GetFileInformationByName = NULL; + static int GetFileInformationByName_init = -1; + + if (GetFileInformationByName_init < 0) { + HMODULE hMod = LoadLibraryW(L"api-ms-win-core-file-l2-1-4"); + GetFileInformationByName_init = 0; + if (hMod) { + GetFileInformationByName = (PGetFileInformationByName)GetProcAddress( + hMod, "GetFileInformationByName"); + if (GetFileInformationByName) { + GetFileInformationByName_init = 1; + } else { + FreeLibrary(hMod); + } + } + } + + if (GetFileInformationByName_init <= 0) { + SetLastError(ERROR_NOT_SUPPORTED); + return FALSE; + } + return GetFileInformationByName(FileName, FileInformationClass, FileInfoBuffer, FileInfoBufferSize); +} + +#endif + +#endif diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 494bcf293cdb7b..2103c3d673cbbc 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1009,6 +1009,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(logoption)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(loop)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(mapping)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(mask)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(match)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(max_length)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(maxdigits)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index b0cb8365933e77..2d3aa40746bd42 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -495,6 +495,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(logoption) STRUCT_FOR_ID(loop) STRUCT_FOR_ID(mapping) + STRUCT_FOR_ID(mask) STRUCT_FOR_ID(match) STRUCT_FOR_ID(max_length) STRUCT_FOR_ID(maxdigits) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 4b128da54555b7..2bb3ee47daf530 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1001,6 +1001,7 @@ extern "C" { INIT_ID(logoption), \ INIT_ID(loop), \ INIT_ID(mapping), \ + INIT_ID(mask), \ INIT_ID(match), \ INIT_ID(max_length), \ INIT_ID(maxdigits), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 7ef1f7e94ddead..f091a883c60886 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -896,6 +896,8 @@ _PyUnicode_InitStaticStrings(void) { PyUnicode_InternInPlace(&string); string = &_Py_ID(mapping); PyUnicode_InternInPlace(&string); + string = &_Py_ID(mask); + PyUnicode_InternInPlace(&string); string = &_Py_ID(match); PyUnicode_InternInPlace(&string); string = &_Py_ID(max_length); diff --git a/Lib/genericpath.py b/Lib/genericpath.py index ce36451a3af01c..4fefcc976834d5 100644 --- a/Lib/genericpath.py +++ b/Lib/genericpath.py @@ -16,7 +16,7 @@ def exists(path): """Test whether a path exists. Returns False for broken symbolic links""" try: - os.stat(path) + os.statx(path, stat.STATX_TYPE) except (OSError, ValueError): return False return True @@ -27,7 +27,7 @@ def exists(path): def isfile(path): """Test whether a path is a regular file""" try: - st = os.stat(path) + st = os.statx(path, stat.STATX_TYPE) except (OSError, ValueError): return False return stat.S_ISREG(st.st_mode) @@ -39,7 +39,7 @@ def isfile(path): def isdir(s): """Return true if the pathname refers to an existing directory.""" try: - st = os.stat(s) + st = os.statx(s, stat.STATX_TYPE) except (OSError, ValueError): return False return stat.S_ISDIR(st.st_mode) @@ -47,21 +47,23 @@ def isdir(s): def getsize(filename): """Return the size of a file, reported by os.stat().""" - return os.stat(filename).st_size + return os.statx(filename, stat.STATX_SIZE).st_size def getmtime(filename): """Return the last modification time of a file, reported by os.stat().""" - return os.stat(filename).st_mtime + return os.statx(filename, stat.STATX_MTIME).st_mtime def getatime(filename): """Return the last access time of a file, reported by os.stat().""" - return os.stat(filename).st_atime + return os.statx(filename, stat.STATX_ATIME).st_atime def getctime(filename): """Return the metadata change time of a file, reported by os.stat().""" + # XXX: If we change to statx, st_ctime on Windows will start returning + # change time instead of creation time. return os.stat(filename).st_ctime @@ -86,6 +88,8 @@ def commonprefix(m): # describing the same file? def samestat(s1, s2): """Test whether two stat buffers reference the same file""" + if not s1.stx_mask & s2.stx_mask & stat.STATX_INO: + raise ValueError("stat values must include STATX_INO") return (s1.st_ino == s2.st_ino and s1.st_dev == s2.st_dev) @@ -97,8 +101,8 @@ def samefile(f1, f2): This is determined by the device number and i-node number and raises an exception if an os.stat() call on either pathname fails. """ - s1 = os.stat(f1) - s2 = os.stat(f2) + s1 = os.statx(f1, stat.STATX_INO) + s2 = os.statx(f2, stat.STATX_INO) return samestat(s1, s2) @@ -106,8 +110,8 @@ def samefile(f1, f2): # (Not necessarily the same file descriptor!) def sameopenfile(fp1, fp2): """Test whether two open file objects reference the same file""" - s1 = os.fstat(fp1) - s2 = os.fstat(fp2) + s1 = os.statx(fp1, stat.STATX_INO) + s2 = os.statx(fp2, stat.STATX_INO) return samestat(s1, s2) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 873c884c3bd934..487bdc5770bb92 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -262,7 +262,7 @@ def islink(path): This will always return false for Windows prior to 6.0. """ try: - st = os.lstat(path) + st = os.statx(path, stat.STATX_TYPE, follow_symlinks=False) except (OSError, ValueError, AttributeError): return False return stat.S_ISLNK(st.st_mode) @@ -274,7 +274,7 @@ def islink(path): def isjunction(path): """Test whether a path is a junction""" try: - st = os.lstat(path) + st = os.statx(path, stat.STATX_TYPE, follow_symlinks=False) except (OSError, ValueError, AttributeError): return False return bool(st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT) @@ -290,7 +290,7 @@ def isjunction(path): def lexists(path): """Test whether a path exists. Returns True for broken symbolic links""" try: - st = os.lstat(path) + st = os.statx(path, stat.STATX_TYPE, follow_symlinks=False) except (OSError, ValueError): return False return True diff --git a/Lib/os.py b/Lib/os.py index fd1e774fdcbcfa..de6324edec42ce 100644 --- a/Lib/os.py +++ b/Lib/os.py @@ -178,7 +178,9 @@ def _add(str, fn): _add("HAVE_LSTAT", "stat") _add("HAVE_FSTATAT", "stat") _add("HAVE_UTIMENSAT", "utime") + _add("HAVE_STATX", "statx") _add("MS_WINDOWS", "stat") + _add("MS_WINDOWS", "statx") supports_follow_symlinks = _set del _set @@ -1086,6 +1088,17 @@ def __subclasshook__(cls, subclass): __class_getitem__ = classmethod(GenericAlias) +# If there is no C implementation, make a pure Python version +if not _exists('statx'): + def statx(path, mask, *, dir_fd=None, follow_symlinks=True, flags=0): + """Perform a stat system call on the given path, retrieving certain information. + +This is a fallback implementation that calls stat() or lstat() based on the +*follow_symlinks* argument. The *mask* argument is ignored and the stx_mask +result will be set for the information returned by a normal stat() call. +""" + return (stat if follow_symlinks else lstat)(path, dir_fd=dir_fd) + if name == 'nt': class _AddedDllDirectory: diff --git a/Lib/stat.py b/Lib/stat.py index fc024db3f4fbee..de19b943f472b9 100644 --- a/Lib/stat.py +++ b/Lib/stat.py @@ -166,6 +166,24 @@ def filemode(mode): return "".join(perm) +# Mask values for statx() +STATX_TYPE = 0x00000001 # includes st_file_attributes and st_reparse_tag +STATX_MODE = 0x00000002 +STATX_NLINK = 0x00000004 +STATX_UID = 0x00000008 +STATX_GID = 0x00000010 +STATX_ATIME = 0x00000020 +STATX_MTIME = 0x00000040 +STATX_CTIME = 0x00000080 +STATX_INO = 0x00000100 # includes st_dev on Windows +STATX_SIZE = 0x00000200 +STATX_BLOCKS = 0x00000400 +STATX_BASIC_STATS = 0x000007ff +STATX_BTIME = 0x00000800 +STATX_MNT_ID = 0x00001000 +STATX_DIOALIGN = 0x00002000 + + # Windows FILE_ATTRIBUTE constants for interpreting os.stat()'s # "st_file_attributes" member diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 94db8bb7737acd..c3be8ea8bb1170 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -531,6 +531,10 @@ def setUp(self): def check_stat_attributes(self, fname): result = os.stat(fname) + # Make sure stx_mask is present and non-zero + # Even for non-statx calls we will set this field + self.assertNotEqual(result.stx_mask, 0) + # Make sure direct access works self.assertEqual(result[stat.ST_SIZE], 3) self.assertEqual(result.st_size, 3) diff --git a/Misc/NEWS.d/next/Library/2022-11-24-14-57-48.gh-issue-99726.eayGOV.rst b/Misc/NEWS.d/next/Library/2022-11-24-14-57-48.gh-issue-99726.eayGOV.rst new file mode 100644 index 00000000000000..86c069a6e0cd80 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-11-24-14-57-48.gh-issue-99726.eayGOV.rst @@ -0,0 +1 @@ +Adds :func:`os.statx` function. diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index f9f6ca372ec6c7..f825cc19eda5d4 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -178,6 +178,133 @@ os_lstat(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kw return return_value; } +PyDoc_STRVAR(os_statx__doc__, +"statx($module, /, path, mask, *, dir_fd=None, follow_symlinks=True,\n" +" flags=0)\n" +"--\n" +"\n" +"Perform a stat system call on the given path, retrieving certain information.\n" +"\n" +" path\n" +" Path to be examined; can be string, bytes, a path-like object or\n" +" open-file-descriptor int.\n" +" mask\n" +" A combination of stat.STATX_* flags specifying the fields that the\n" +" caller is interested in. The stx_mask member of the result will\n" +" include all fields that are actually filled in, which may be more\n" +" or fewer than those specified in this argument.\n" +" dir_fd\n" +" If not None, it should be a file descriptor open to a directory,\n" +" and path should be a relative string; path will then be relative to\n" +" that directory.\n" +" follow_symlinks\n" +" If False, and the last element of the path is a symbolic link,\n" +" stat will examine the symbolic link itself instead of the file\n" +" the link points to.\n" +" flags\n" +" A combination of AT_* flags specifying how path should be resolved.\n" +" These are only relevant on Linux.\n" +"\n" +"dir_fd and follow_symlinks may not be implemented\n" +" on your platform. If they are unavailable, using them will raise a\n" +" NotImplementedError.\n" +"\n" +"It\'s an error to use dir_fd or follow_symlinks when specifying path as\n" +" an open file descriptor.\n" +"\n" +"The follow_symlinks parameter adds the AT_SYMLINK_NOFOLLOW flag into flags\n" +" (when passed False) but will not remove it. Using this parameter rather\n" +" than the flag is recommended for maximum portability."); + +#define OS_STATX_METHODDEF \ + {"statx", _PyCFunction_CAST(os_statx), METH_FASTCALL|METH_KEYWORDS, os_statx__doc__}, + +static PyObject * +os_statx_impl(PyObject *module, path_t *path, int mask, int dir_fd, + int follow_symlinks, int flags); + +static PyObject * +os_statx(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 5 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_item = { &_Py_ID(path), &_Py_ID(mask), &_Py_ID(dir_fd), &_Py_ID(follow_symlinks), &_Py_ID(flags), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"path", "mask", "dir_fd", "follow_symlinks", "flags", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "statx", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[5]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 2; + path_t path = PATH_T_INITIALIZE("statx", "path", 0, 1); + int mask; + int dir_fd = DEFAULT_DIR_FD; + int follow_symlinks = 1; + int flags = 0; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 2, 2, 0, argsbuf); + if (!args) { + goto exit; + } + if (!path_converter(args[0], &path)) { + goto exit; + } + mask = _PyLong_AsInt(args[1]); + if (mask == -1 && PyErr_Occurred()) { + goto exit; + } + if (!noptargs) { + goto skip_optional_kwonly; + } + if (args[2]) { + if (!dir_fd_converter(args[2], &dir_fd)) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + if (args[3]) { + follow_symlinks = PyObject_IsTrue(args[3]); + if (follow_symlinks < 0) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + flags = _PyLong_AsInt(args[4]); + if (flags == -1 && PyErr_Occurred()) { + goto exit; + } +skip_optional_kwonly: + return_value = os_statx_impl(module, &path, mask, dir_fd, follow_symlinks, flags); + +exit: + /* Cleanup for path */ + path_cleanup(&path); + + return return_value; +} + PyDoc_STRVAR(os_access__doc__, "access($module, /, path, mode, *, dir_fd=None, effective_ids=False,\n" " follow_symlinks=True)\n" @@ -11549,4 +11676,4 @@ os_waitstatus_to_exitcode(PyObject *module, PyObject *const *args, Py_ssize_t na #ifndef OS_WAITSTATUS_TO_EXITCODE_METHODDEF #define OS_WAITSTATUS_TO_EXITCODE_METHODDEF #endif /* !defined(OS_WAITSTATUS_TO_EXITCODE_METHODDEF) */ -/*[clinic end generated code: output=4192d8e09e216300 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=aeba1208190afb68 input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 8185517b06b5dd..97a8aa95559731 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -36,6 +36,7 @@ # include "posixmodule.h" #else # include "winreparse.h" +# include "pycore_fileutils_windows.h" // GetFileInformationByName() #endif #if !defined(EX_OK) && defined(EXIT_SUCCESS) @@ -664,6 +665,8 @@ PyOS_AfterFork(void) void _Py_time_t_to_FILE_TIME(time_t, int, FILETIME *); void _Py_attribute_data_to_stat(BY_HANDLE_FILE_INFORMATION *, ULONG, struct _Py_stat_struct *); +void _Py_stat_basic_info_to_stat(FILE_STAT_BASIC_INFORMATION *, + struct _Py_stat_struct *); #endif @@ -2056,6 +2059,52 @@ win32_stat(const wchar_t* path, struct _Py_stat_struct *result) return win32_xstat(path, result, TRUE); } + +/* The statx values we can get from FileStatBasicByNameInfo. + Notably absent are STATX_INO (because we can't get st_dev efficiently), + and STATX_MODE. + Any flags requested that aren't in this mask will cause the FILE_STATS + below to be used, even if it is missing other flags. */ +#define WIN32_STATX_BASIC_STATS (0x0EC5) +/* The statx values we get from GetFileInformationByHandle(). + Notably absent is STATX_CTIME (we return creation time instead).*/ +#define WIN32_STATX_FILE_STATS (0x0F67) + +static int +win32_statx(const wchar_t *path, struct _Py_stat_struct *result, + int mask, int follow_symlinks, unsigned int *result_mask) +{ + if (!(mask & ~WIN32_STATX_BASIC_STATS)) { + /* Try and use our fast path to fill in the result */ + FILE_STAT_BASIC_INFORMATION statInfo; + if (GetFileInformationByName(path, FileStatBasicByNameInfo, + &statInfo, sizeof(statInfo))) { + if (// Cannot use fast path for reparse points ... + !(statInfo.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + // ... unless it's a name surrogate (symlink) and we're not following + || (!follow_symlinks && IsReparseTagNameSurrogate(statInfo.ReparseTag)) + ) { + *result_mask = WIN32_STATX_BASIC_STATS; + _Py_stat_basic_info_to_stat(&statInfo, result); + } + } else { + /* Some errors aren't worth retrying with the slow path */ + switch(GetLastError()) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_NOT_READY: + case ERROR_BAD_NET_NAME: + return -1; + } + } + } + + /* No result yet, so either we need to follow a symlink, or the + user requested more information than our fast path offers */ + *result_mask = WIN32_STATX_FILE_STATS; + return win32_xstat(path, result, follow_symlinks); +} + #endif /* MS_WINDOWS */ PyDoc_STRVAR(stat_result__doc__, @@ -2087,22 +2136,22 @@ static PyStructSequence_Field stat_result_fields[] = { {"st_atime_ns", "time of last access in nanoseconds"}, {"st_mtime_ns", "time of last modification in nanoseconds"}, {"st_ctime_ns", "time of last change in nanoseconds"}, -#ifdef HAVE_STRUCT_STAT_ST_BLKSIZE +#if defined(HAVE_STRUCT_STAT_ST_BLKSIZE) || defined(HAVE_STATX) {"st_blksize", "blocksize for filesystem I/O"}, #endif -#ifdef HAVE_STRUCT_STAT_ST_BLOCKS +#if defined(HAVE_STRUCT_STAT_ST_BLOCKS) || defined(HAVE_STATX) {"st_blocks", "number of blocks allocated"}, #endif -#ifdef HAVE_STRUCT_STAT_ST_RDEV +#if defined(HAVE_STRUCT_STAT_ST_RDEV) || defined(HAVE_STATX) {"st_rdev", "device type (if inode device)"}, #endif -#ifdef HAVE_STRUCT_STAT_ST_FLAGS +#if defined(HAVE_STRUCT_STAT_ST_FLAGS) || defined(HAVE_STATX) {"st_flags", "user defined flags for file"}, #endif #ifdef HAVE_STRUCT_STAT_ST_GEN {"st_gen", "generation number"}, #endif -#ifdef HAVE_STRUCT_STAT_ST_BIRTHTIME +#if defined(HAVE_STRUCT_STAT_ST_BIRTHTIME) || defined(HAVE_STATX) {"st_birthtime", "time of creation"}, #endif #ifdef HAVE_STRUCT_STAT_ST_FILE_ATTRIBUTES @@ -2113,29 +2162,36 @@ static PyStructSequence_Field stat_result_fields[] = { #endif #ifdef HAVE_STRUCT_STAT_ST_REPARSE_TAG {"st_reparse_tag", "Windows reparse tag"}, +#endif + /* the stx_mask attribute is always present to allow for fallbacks */ + {"stx_mask", "mask of fields provided by the system"}, +#ifdef HAVE_STATX + {"stx_attributes", "additional file attributes"}, + {"stx_attributes_mask", "mask indicating which attributes are supported"}, + {"stx_mnt_id", "the mount id"}, #endif {0} }; -#ifdef HAVE_STRUCT_STAT_ST_BLKSIZE +#if defined(HAVE_STRUCT_STAT_ST_BLKSIZE) || defined(HAVE_STATX) #define ST_BLKSIZE_IDX 16 #else #define ST_BLKSIZE_IDX 15 #endif -#ifdef HAVE_STRUCT_STAT_ST_BLOCKS +#if defined(HAVE_STRUCT_STAT_ST_BLOCKS) || defined(HAVE_STATX) #define ST_BLOCKS_IDX (ST_BLKSIZE_IDX+1) #else #define ST_BLOCKS_IDX ST_BLKSIZE_IDX #endif -#ifdef HAVE_STRUCT_STAT_ST_RDEV +#if defined(HAVE_STRUCT_STAT_ST_RDEV) || defined(HAVE_STATX) #define ST_RDEV_IDX (ST_BLOCKS_IDX+1) #else #define ST_RDEV_IDX ST_BLOCKS_IDX #endif -#ifdef HAVE_STRUCT_STAT_ST_FLAGS +#if defined(HAVE_STRUCT_STAT_ST_FLAGS) || defined(HAVE_STATX) #define ST_FLAGS_IDX (ST_RDEV_IDX+1) #else #define ST_FLAGS_IDX ST_RDEV_IDX @@ -2147,7 +2203,7 @@ static PyStructSequence_Field stat_result_fields[] = { #define ST_GEN_IDX ST_FLAGS_IDX #endif -#ifdef HAVE_STRUCT_STAT_ST_BIRTHTIME +#if defined(HAVE_STRUCT_STAT_ST_BIRTHTIME) || defined(HAVE_STATX) #define ST_BIRTHTIME_IDX (ST_GEN_IDX+1) #else #define ST_BIRTHTIME_IDX ST_GEN_IDX @@ -2171,6 +2227,23 @@ static PyStructSequence_Field stat_result_fields[] = { #define ST_REPARSE_TAG_IDX ST_FSTYPE_IDX #endif +#define STX_MASK_IDX (ST_REPARSE_TAG_IDX+1) + +#ifdef HAVE_STATX +#define STX_ATTRIBUTES_IDX (STX_MASK_IDX+1) +#define STX_ATTRIBUTES_MASK_IDX (STX_ATTRIBUTES_IDX+1) +#define STX_MNT_ID_IDX (STX_ATTRIBUTES_MASK_IDX+1) +#endif + +/* for when regular stat() gets called */ +#ifdef MS_WINDOWS +#define ST_DEFAULT_STX_MASK WIN32_STATX_FILE_STATS +#else +/* STATX_BASIC_STATS */ +#define ST_DEFAULT_STX_MASK 0x000007ff +#endif + + static PyStructSequence_Desc stat_result_desc = { "stat_result", /* name */ stat_result__doc__, /* doc */ @@ -2354,7 +2427,7 @@ fill_time(PyObject *module, PyObject *v, int index, time_t sec, unsigned long ns /* pack a system stat C structure into the Python stat tuple (used by posix_stat() and posix_fstat()) */ static PyObject* -_pystat_fromstructstat(PyObject *module, STRUCT_STAT *st) +_pystat_fromstructstat(PyObject *module, STRUCT_STAT *st, unsigned int stx_mask) { unsigned long ansec, mnsec, cnsec; PyObject *StatResultType = get_posix_state(module)->StatResultType; @@ -2421,12 +2494,18 @@ _pystat_fromstructstat(PyObject *module, STRUCT_STAT *st) #ifdef HAVE_STRUCT_STAT_ST_BIRTHTIME { PyObject *val; - unsigned long bsec,bnsec; - bsec = (long)st->st_birthtime; + double bsec; + unsigned long bnsec; +#ifdef MS_WINDOWS + bsec = (double)st->st_btime; + bnsec = st->st_btime_nsec; +#else + bsec = (double)(long)st->st_birthtime; #ifdef HAVE_STAT_TV_NSEC2 bnsec = st->st_birthtimespec.tv_nsec; #else bnsec = 0; +#endif #endif val = PyFloat_FromDouble(bsec + 1e-9*bnsec); PyStructSequence_SET_ITEM(v, ST_BIRTHTIME_IDX, @@ -2450,6 +2529,12 @@ _pystat_fromstructstat(PyObject *module, STRUCT_STAT *st) PyLong_FromUnsignedLong(st->st_reparse_tag)); #endif + if (!stx_mask) { + stx_mask = ST_DEFAULT_STX_MASK; + } + PyStructSequence_SET_ITEM(v, STX_MASK_IDX, + PyLong_FromUnsignedLong(stx_mask)); + if (PyErr_Occurred()) { Py_DECREF(v); return NULL; @@ -2458,6 +2543,70 @@ _pystat_fromstructstat(PyObject *module, STRUCT_STAT *st) return v; } +#ifdef HAVE_STATX + +/* pack a system statx C structure into the Python stat tuple + (used by posix_statx()) */ +static PyObject* +_pystat_fromstructstatx(PyObject *module, struct statx *st) +{ + unsigned long long dev; + PyObject *StatResultType = get_posix_state(module)->StatResultType; + PyObject *v = PyStructSequence_New((PyTypeObject *)StatResultType); + if (v == NULL) + return NULL; + + PyStructSequence_SET_ITEM(v, 0, PyLong_FromLong((long)st->stx_mode)); + static_assert(sizeof(unsigned long long) >= sizeof(st->stx_ino), + "statx.stx_ino is larger than unsigned long long"); + PyStructSequence_SET_ITEM(v, 1, PyLong_FromUnsignedLongLong(st->stx_ino)); + dev = (unsigned long long)st->stx_dev_major << 32 | st->stx_dev_minor; + PyStructSequence_SET_ITEM(v, 2, PyLong_FromUnsignedLongLong(dev)); + PyStructSequence_SET_ITEM(v, 3, PyLong_FromUnsignedLong(st->stx_nlink)); + PyStructSequence_SET_ITEM(v, 4, _PyLong_FromUid(st->stx_uid)); + PyStructSequence_SET_ITEM(v, 5, _PyLong_FromGid(st->stx_gid)); + static_assert(sizeof(unsigned long long) >= sizeof(st->stx_size), + "statx.stx_size is larger than unsigned long long"); + PyStructSequence_SET_ITEM(v, 6, PyLong_FromUnsignedLongLong(st->stx_size)); + + fill_time(module, v, 7, st->stx_atime.tv_sec, st->stx_atime.tv_nsec); + fill_time(module, v, 8, st->stx_mtime.tv_sec, st->stx_mtime.tv_nsec); + fill_time(module, v, 9, st->stx_ctime.tv_sec, st->stx_ctime.tv_nsec); + + PyStructSequence_SET_ITEM(v, ST_BLKSIZE_IDX, + PyLong_FromUnsignedLong(st->stx_blksize)); + PyStructSequence_SET_ITEM(v, ST_BLOCKS_IDX, + PyLong_FromUnsignedLongLong(st->stx_blocks)); + dev = (unsigned long long)st->stx_rdev_major << 32 | st->stx_rdev_minor; + PyStructSequence_SET_ITEM(v, ST_RDEV_IDX, + PyLong_FromUnsignedLongLong(dev)); + + { + PyObject *btime; + btime = PyFloat_FromDouble(st->stx_btime.tv_sec + 1e-9*st->stx_btime.tv_nsec); + PyStructSequence_SET_ITEM(v, ST_BIRTHTIME_IDX, btime); + } + + PyStructSequence_SET_ITEM(v, STX_MASK_IDX, + PyLong_FromUnsignedLong(st->stx_mask)); + PyStructSequence_SET_ITEM(v, STX_ATTRIBUTES_IDX, + PyLong_FromUnsignedLongLong(st->stx_attributes)); + PyStructSequence_SET_ITEM(v, STX_ATTRIBUTES_MASK_IDX, + PyLong_FromUnsignedLongLong(st->stx_attributes_mask)); + PyStructSequence_SET_ITEM(v, STX_MNT_ID_IDX, + PyLong_FromUnsignedLongLong(st->stx_mnt_id)); + + if (PyErr_Occurred()) { + Py_DECREF(v); + return NULL; + } + + return v; +} + +#endif + + /* POSIX methods */ @@ -2467,6 +2616,7 @@ posix_do_stat(PyObject *module, const char *function_name, path_t *path, { STRUCT_STAT st; int result; + unsigned int result_mask = ST_DEFAULT_STX_MASK; #ifdef HAVE_FSTATAT int fstatat_unavailable = 0; @@ -2523,9 +2673,46 @@ posix_do_stat(PyObject *module, const char *function_name, path_t *path, return path_error(path); } - return _pystat_fromstructstat(module, &st); + return _pystat_fromstructstat(module, &st, 0); +} + + +#ifdef HAVE_STATX + +static PyObject * +posix_do_statx(PyObject *module, path_t *path, unsigned int mask, + int dir_fd, int flags) +{ + struct statx st; + int result; + + if (path_and_dir_fd_invalid("statx", path, dir_fd) || + dir_fd_and_fd_invalid("statx", dir_fd, path->fd) || + fd_and_follow_symlinks_invalid("statx", path->fd, !(flags & AT_SYMLINK_NOFOLLOW))) + return NULL; + + if (dir_fd == DEFAULT_DIR_FD) { + dir_fd = AT_FDCWD; + } + + Py_BEGIN_ALLOW_THREADS + if (path->fd != -1) { + result = statx(path->fd, "", flags | AT_EMPTY_PATH, mask, &st); + } else { + result = statx(dir_fd, path->narrow, flags, mask, &st); + } + Py_END_ALLOW_THREADS + + if (result != 0) { + return path_error(path); + } + + return _pystat_fromstructstatx(module, &st); } +#endif + + /*[python input] for s in """ @@ -2879,6 +3066,91 @@ os_lstat_impl(PyObject *module, path_t *path, int dir_fd) return posix_do_stat(module, "lstat", path, dir_fd, follow_symlinks); } +/*[clinic input] + +os.statx + + path : path_t(allow_fd=True) + Path to be examined; can be string, bytes, a path-like object or + open-file-descriptor int. + + mask : int + A combination of stat.STATX_* flags specifying the fields that the + caller is interested in. The stx_mask member of the result will + include all fields that are actually filled in, which may be more + or fewer than those specified in this argument. + + * + + dir_fd : dir_fd = None + If not None, it should be a file descriptor open to a directory, + and path should be a relative string; path will then be relative to + that directory. + + follow_symlinks: bool = True + If False, and the last element of the path is a symbolic link, + stat will examine the symbolic link itself instead of the file + the link points to. + + flags : int = 0 + A combination of AT_* flags specifying how path should be resolved. + These are only relevant on Linux. + + +Perform a stat system call on the given path, retrieving certain information. + +dir_fd and follow_symlinks may not be implemented + on your platform. If they are unavailable, using them will raise a + NotImplementedError. + +It's an error to use dir_fd or follow_symlinks when specifying path as + an open file descriptor. + +The follow_symlinks parameter adds the AT_SYMLINK_NOFOLLOW flag into flags + (when passed False) but will not remove it. Using this parameter rather + than the flag is recommended for maximum portability. + +[clinic start generated code]*/ + +static PyObject * +os_statx_impl(PyObject *module, path_t *path, int mask, int dir_fd, + int follow_symlinks, int flags) +/*[clinic end generated code: output=e38f7e693d96b2c6 input=5e832dfc79a58fb7]*/ +{ +#ifdef MS_WINDOWS + struct _Py_stat_struct st; + unsigned int result_mask; + int result; + + Py_BEGIN_ALLOW_THREADS + if (path->fd != -1) { + _Py_BEGIN_SUPPRESS_IPH + result = FSTAT(path->fd, &st); + result_mask = ST_DEFAULT_STX_MASK; + _Py_END_SUPPRESS_IPH + } else { + result = win32_statx(path->wide, &st, mask, follow_symlinks, &result_mask); + } + Py_END_ALLOW_THREADS + + if (result != 0) { + return path_error(path); + } + + return _pystat_fromstructstat(module, &st, result_mask); + +#else /* MS_WINDOWS */ + +#ifdef AT_SYMLINK_NOFOLLOW + if (!follow_symlinks) { + flags |= AT_SYMLINK_NOFOLLOW; + } +#endif + + return posix_do_statx(module, "stat", path, dir_fd, follow_symlinks); +#endif +} + /*[clinic input] os.access -> bool @@ -10190,7 +10462,7 @@ os_fstat_impl(PyObject *module, int fd) #endif } - return _pystat_fromstructstat(module, &st); + return _pystat_fromstructstat(module, &st, 0); } @@ -13701,7 +13973,7 @@ DirEntry_fetch_stat(PyObject *module, DirEntry *self, int follow_symlinks) if (result != 0) return path_object_error(self->path); - return _pystat_fromstructstat(module, &st); + return _pystat_fromstructstat(module, &st, 0); } static PyObject * @@ -13710,7 +13982,7 @@ DirEntry_get_lstat(PyTypeObject *defining_class, DirEntry *self) if (!self->lstat) { PyObject *module = PyType_GetModule(defining_class); #ifdef MS_WINDOWS - self->lstat = _pystat_fromstructstat(module, &self->win32_lstat); + self->lstat = _pystat_fromstructstat(module, &self->win32_lstat, 0); #else /* POSIX */ self->lstat = DirEntry_fetch_stat(module, self, 0); #endif @@ -14851,6 +15123,7 @@ os_waitstatus_to_exitcode_impl(PyObject *module, PyObject *status_obj) static PyMethodDef posix_methods[] = { OS_STAT_METHODDEF + OS_STATX_METHODDEF OS_ACCESS_METHODDEF OS_TTYNAME_METHODDEF OS_CHDIR_METHODDEF diff --git a/PC/pyconfig.h b/PC/pyconfig.h index 1a33d4c5a1e4fc..aaa0e0b36c3fcd 100644 --- a/PC/pyconfig.h +++ b/PC/pyconfig.h @@ -704,4 +704,7 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */ /* Define if libssl has X509_VERIFY_PARAM_set1_host and related function */ #define HAVE_X509_VERIFY_PARAM_SET1_HOST 1 +/* We add st_birthtime on Windows for ftCreationTime */ +#define HAVE_STRUCT_STAT_ST_BIRTHTIME 1 + #endif /* !Py_CONFIG_H */ diff --git a/Python/fileutils.c b/Python/fileutils.c index 244bd899b3bd24..56f0d049f58d5e 100644 --- a/Python/fileutils.c +++ b/Python/fileutils.c @@ -9,6 +9,7 @@ # include # include # include // PathCchCombineEx +# include "pycore_fileutils_windows.h" // FILE_STAT_BASIC_INFORMATION extern int winerror_to_errno(int); #endif @@ -1048,6 +1049,13 @@ FILE_TIME_to_time_t_nsec(FILETIME *in_ptr, time_t *time_out, int* nsec_out) *time_out = Py_SAFE_DOWNCAST((in / 10000000) - secs_between_epochs, __int64, time_t); } +static void +LARGE_INTEGER_to_time_t_nsec(LARGE_INTEGER *in_ptr, time_t *time_out, int* nsec_out) +{ + *nsec_out = (int)(in_ptr->QuadPart % 10000000) * 100; /* FILETIME is in units of 100 nsec. */ + *time_out = Py_SAFE_DOWNCAST((in_ptr->QuadPart / 10000000) - secs_between_epochs, __int64, time_t); +} + void _Py_time_t_to_FILE_TIME(time_t time_in, int nsec_in, FILETIME *out_ptr) { @@ -1086,7 +1094,11 @@ _Py_attribute_data_to_stat(BY_HANDLE_FILE_INFORMATION *info, ULONG reparse_tag, result->st_size = (((__int64)info->nFileSizeHigh)<<32) + info->nFileSizeLow; result->st_dev = info->dwVolumeSerialNumber; result->st_rdev = result->st_dev; - FILE_TIME_to_time_t_nsec(&info->ftCreationTime, &result->st_ctime, &result->st_ctime_nsec); + FILE_TIME_to_time_t_nsec(&info->ftCreationTime, &result->st_btime, &result->st_btime_nsec); + /* ctime is the change time, not the creation (birth) time, + but we used to set it, so we'd best keep doing it. */ + result->st_ctime = result->st_btime; + result->st_ctime_nsec = result->st_ctime_nsec; FILE_TIME_to_time_t_nsec(&info->ftLastWriteTime, &result->st_mtime, &result->st_mtime_nsec); FILE_TIME_to_time_t_nsec(&info->ftLastAccessTime, &result->st_atime, &result->st_atime_nsec); result->st_nlink = info->nNumberOfLinks; @@ -1104,6 +1116,51 @@ _Py_attribute_data_to_stat(BY_HANDLE_FILE_INFORMATION *info, ULONG reparse_tag, } result->st_file_attributes = info->dwFileAttributes; } + +void +_Py_stat_basic_info_to_stat(FILE_STAT_BASIC_INFORMATION *info, + struct _Py_stat_struct *result) +{ + /* set values here to match WIN32_STATX_BASIC_STATS defined in posixmodule.c */ + memset(result, 0, sizeof(*result)); + result->st_mode = attributes_to_mode(info->FileAttributes); + result->st_size = info->EndOfFile.QuadPart; + LARGE_INTEGER_to_time_t_nsec(&info->CreationTime, &result->st_btime, &result->st_btime_nsec); + LARGE_INTEGER_to_time_t_nsec(&info->ChangeTime, &result->st_ctime, &result->st_ctime_nsec); + LARGE_INTEGER_to_time_t_nsec(&info->LastWriteTime, &result->st_mtime, &result->st_mtime_nsec); + LARGE_INTEGER_to_time_t_nsec(&info->LastAccessTime, &result->st_atime, &result->st_atime_nsec); + result->st_nlink = info->NumberOfLinks; + result->st_ino = info->FileId.QuadPart; + /* bpo-37834: Only actual symlinks set the S_IFLNK flag. But lstat() will + open other name surrogate reparse points without traversing them. To + detect/handle these, check st_file_attributes and st_reparse_tag. */ + result->st_reparse_tag = info->ReparseTag; + if (info->FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT && + info->ReparseTag == IO_REPARSE_TAG_SYMLINK) { + /* set the bits that make this a symlink */ + result->st_mode = (result->st_mode & ~S_IFMT) | S_IFLNK; + } + result->st_file_attributes = info->FileAttributes; + switch (info->DeviceType) { + case FILE_TYPE_DISK: + break; + case FILE_TYPE_CHAR: + /* \\.\nul */ + result->st_mode = (result->st_mode & ~S_IFMT) | _S_IFCHR; + break; + case FILE_TYPE_PIPE: + /* \\.\pipe\spam */ + result->st_mode = (result->st_mode & ~S_IFMT) | _S_IFIFO; + break; + default: + if (info->FileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + /* \\.\pipe\ or \\.\mailslot\ */ + result->st_mode = (result->st_mode & ~S_IFMT) | _S_IFDIR; + } + break; + } +} + #endif /* Return information about a file. diff --git a/configure.ac b/configure.ac index 19f12e8d5402fb..60fa129b37111e 100644 --- a/configure.ac +++ b/configure.ac @@ -4789,7 +4789,7 @@ AC_CHECK_FUNCS([ \ setitimer setlocale setpgid setpgrp setpriority setregid setresgid \ setresuid setreuid setsid setuid setvbuf shutdown sigaction sigaltstack \ sigfillset siginterrupt sigpending sigrelse sigtimedwait sigwait \ - sigwaitinfo snprintf splice strftime strlcpy strsignal symlinkat sync \ + sigwaitinfo snprintf splice statx strftime strlcpy strsignal symlinkat sync \ sysconf system tcgetpgrp tcsetpgrp tempnam timegm times tmpfile \ tmpnam tmpnam_r truncate ttyname umask uname unlinkat utimensat utimes vfork \ wait wait3 wait4 waitid waitpid wcscoll wcsftime wcsxfrm wmemcmp writev \ diff --git a/pyconfig.h.in b/pyconfig.h.in index 236cee6588d49b..03537cad055908 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -1181,6 +1181,9 @@ /* Define to 1 if you have the `statvfs' function. */ #undef HAVE_STATVFS +/* Define to 1 if you have the `statx' function. */ +#undef HAVE_STATX + /* Define if you have struct stat.st_mtim.tv_nsec */ #undef HAVE_STAT_TV_NSEC