macOS: retrieve birthtime in nanosecond precision via system call, fixes #8724

This commit is contained in:
Xiaocheng Song 2025-04-14 11:02:21 +08:00
parent 0fe038192c
commit a6be4c4f6c
4 changed files with 62 additions and 10 deletions

View file

@ -33,7 +33,7 @@ from .helpers import Manifest
from .helpers import hardlinkable
from .helpers import ChunkIteratorFileWrapper, normalize_chunker_params, open_item
from .helpers import Error, IntegrityError, set_ec
from .platform import uid2user, user2uid, gid2group, group2gid
from .platform import uid2user, user2uid, gid2group, group2gid, get_birthtime_ns
from .helpers import parse_timestamp, to_localtime
from .helpers import OutputTimestamp, format_timedelta, format_file_size, file_status, FileSize
from .helpers import safe_encode, safe_decode, make_path_safe, remove_surrogates
@ -1139,7 +1139,7 @@ class MetadataCollector:
self.noxattrs = noxattrs
self.nobirthtime = nobirthtime
def stat_simple_attrs(self, st):
def stat_simple_attrs(self, st, path, fd=None):
attrs = dict(
mode=st.st_mode,
uid=st.st_uid,
@ -1153,9 +1153,10 @@ class MetadataCollector:
attrs['atime'] = safe_ns(st.st_atime_ns)
if not self.noctime:
attrs['ctime'] = safe_ns(st.st_ctime_ns)
if not self.nobirthtime and hasattr(st, 'st_birthtime'):
# sadly, there's no stat_result.st_birthtime_ns
attrs['birthtime'] = safe_ns(int(st.st_birthtime * 10**9))
if not self.nobirthtime:
birthtime_ns = get_birthtime_ns(st, path, fd=fd)
if birthtime_ns is not None:
attrs['birthtime'] = safe_ns(birthtime_ns)
if self.numeric_ids:
attrs['user'] = attrs['group'] = None
else:
@ -1185,7 +1186,7 @@ class MetadataCollector:
return attrs
def stat_attrs(self, st, path, fd=None):
attrs = self.stat_simple_attrs(st)
attrs = self.stat_simple_attrs(st, path, fd=fd)
attrs.update(self.stat_ext_attrs(st, path, fd=fd))
return attrs
@ -1434,7 +1435,7 @@ class FilesystemObjectProcessors:
with OsOpen(path=path, parent_fd=parent_fd, name=name, flags=flags, noatime=True) as fd:
with backup_io('fstat'):
st = stat_update_check(st, os.fstat(fd))
item.update(self.metadata_collector.stat_simple_attrs(st))
item.update(self.metadata_collector.stat_simple_attrs(st, path, fd=fd))
is_special_file = is_special(st.st_mode)
if is_special_file:
# we process a special file like a regular file. reflect that in mode,

View file

@ -40,3 +40,16 @@ elif is_darwin: # pragma: darwin only
from .darwin import API_VERSION as OS_API_VERSION
from .darwin import listxattr, getxattr, setxattr
from .darwin import acl_get, acl_set
from .darwin import is_darwin_feature_64_bit_inode, _get_birthtime_ns
def get_birthtime_ns(st, path, fd=None):
if hasattr(st, "st_birthtime_ns"):
# added in Python 3.12 but not always available.
return st.st_birthtime_ns
elif is_darwin and is_darwin_feature_64_bit_inode:
return _get_birthtime_ns(fd or path, follow_symlinks=False)
elif hasattr(st, "st_birthtime"):
return int(st.st_birthtime * 10**9)
else:
return None

View file

@ -2,6 +2,7 @@ import os
from libc.stdint cimport uint32_t
from libc cimport errno
from posix.time cimport timespec
from .posix import user2uid, group2gid
from ..helpers import safe_decode, safe_encode
@ -9,6 +10,18 @@ from .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_str
API_VERSION = '1.4_01'
cdef extern from *:
"""
#ifdef _DARWIN_FEATURE_64_BIT_INODE
#define DARWIN_FEATURE_64_BIT_INODE_DEFINED 1
#else
#define DARWIN_FEATURE_64_BIT_INODE_DEFINED 0
#endif
"""
int DARWIN_FEATURE_64_BIT_INODE_DEFINED
is_darwin_feature_64_bit_inode = DARWIN_FEATURE_64_BIT_INODE_DEFINED != 0
cdef extern from "sys/xattr.h":
ssize_t c_listxattr "listxattr" (const char *path, char *list, size_t size, int flags)
ssize_t c_flistxattr "flistxattr" (int filedes, char *list, size_t size, int flags)
@ -37,6 +50,14 @@ cdef extern from "sys/acl.h":
char *acl_to_text(acl_t acl, ssize_t *len_p)
int ACL_TYPE_EXTENDED
cdef extern from "sys/stat.h":
cdef struct stat:
timespec st_birthtimespec
int c_stat "stat" (const char *path, stat *buf)
int c_lstat "lstat" (const char *path, stat *buf)
int c_fstat "fstat" (int filedes, stat *buf)
def listxattr(path, *, follow_symlinks=False):
def func(path, buf, size):
@ -159,3 +180,20 @@ def acl_set(path, item, numeric_ids=False, fd=None):
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
finally:
acl_free(acl)
def _get_birthtime_ns(path, follow_symlinks=False):
if isinstance(path, str):
path = os.fsencode(path)
cdef stat stat_info
cdef int result
if isinstance(path, int):
result = c_fstat(path, &stat_info)
else:
if follow_symlinks:
result = c_stat(path, &stat_info)
else:
result = c_lstat(path, &stat_info)
if result != 0:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
return stat_info.st_birthtimespec.tv_sec * 1_000_000_000 + stat_info.st_birthtimespec.tv_nsec

View file

@ -1516,19 +1516,19 @@ class ArchiverTestCase(ArchiverTestCaseBase):
input_path = os.path.abspath('input/file')
xa_key, xa_value = b'com.apple.ResourceFork', b'whatshouldbehere' # issue #7234
xattr.setxattr(input_path.encode(), xa_key, xa_value)
birthtime_expected = os.stat(input_path).st_birthtime
birthtime_expected = platform.get_birthtime_ns(os.stat(input_path), input_path)
mtime_expected = os.stat(input_path).st_mtime_ns
# atime_expected = os.stat(input_path).st_atime_ns
self.cmd('create', self.repository_location + '::test', 'input')
with changedir('output'):
self.cmd('extract', self.repository_location + '::test')
extracted_path = os.path.abspath('input/file')
birthtime_extracted = os.stat(extracted_path).st_birthtime
birthtime_extracted = platform.get_birthtime_ns(os.stat(extracted_path), extracted_path)
mtime_extracted = os.stat(extracted_path).st_mtime_ns
# atime_extracted = os.stat(extracted_path).st_atime_ns
xa_value_extracted = xattr.getxattr(extracted_path.encode(), xa_key)
assert xa_value_extracted == xa_value
assert same_ts_ns(birthtime_extracted * 1e9, birthtime_expected * 1e9)
assert same_ts_ns(birthtime_extracted, birthtime_expected)
assert same_ts_ns(mtime_extracted, mtime_expected)
# assert same_ts_ns(atime_extracted, atime_expected) # still broken, but not really important.