compat/posix: introduce utimensat(2) wrapper

In POSIX.1-2008, utime(3p) was marked as obsolescent in favor of
utimensat(2) and futimens(2). In the recent POSIX.1-2024 (Issue 8)
specification, <utime.h> and utime(3p) were officially removed.

utimensat(2) operates on `struct timespec` rather than the second-only
`struct utimbuf`, allowing sub-second timestamp updates while also
providing support for UTIME_NOW and UTIME_OMIT flags to selectively
update or preserve individual access and modification timestamps.

Introduce a compatibility layer for utimensat(2):
- Provide fallback definitions for AT_FDCWD, UTIME_NOW, and UTIME_OMIT
  in case the system headers lack them.
- Introduce `ST_ATIME_NSEC(st)` to complement `ST_MTIME_NSEC(st)` and
  `ST_CTIME_NSEC(st)`.
- Implement `git_utimensat()` in `compat/utimensat.c` as a fallback using
  utimes(2) on platforms that define NO_UTIMENSAT.
- Implement `mingw_utimensat()` in `compat/mingw.c` converting `struct
  timespec` to Windows FILETIME with 100ns precision.
- Wire up NO_UTIMENSAT support in Makefile, meson.build,
  contrib/buildsystems/CMakeLists.txt, and configure.ac.

Subsequent commits will migrate callers across the codebase to
utimensat(2) and drop the legacy <utime.h> header.

Signed-off-by: Alexey Samsonov <vonosmas@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
jch
Alexey Samsonov 2026-08-21 14:23:21 +00:00 committed by Junio C Hamano
parent 1a3e64c6c4
commit be2b0a8f00
8 changed files with 127 additions and 9 deletions

View File

@ -70,6 +70,8 @@ include shared.mak
#
# Define NO_MKDTEMP if you don't have mkdtemp in the C library.
#
# Define NO_UTIMENSAT if you don't have utimensat.
#
# Define MKDIR_WO_TRAILING_SLASH if your mkdir() can't deal with trailing slash.
#
# Define NO_GECOS_IN_PWENT if you don't have pw_gecos in struct passwd
@ -2049,6 +2051,10 @@ ifdef NO_WRITEV
COMPAT_CFLAGS += -DNO_WRITEV
COMPAT_OBJS += compat/writev.o
endif
ifdef NO_UTIMENSAT
COMPAT_CFLAGS += -DNO_UTIMENSAT
COMPAT_OBJS += compat/utimensat.o
endif
ifdef NO_FAST_WORKING_DIRECTORY
BASIC_CFLAGS += -DNO_FAST_WORKING_DIRECTORY
endif

View File

@ -386,6 +386,8 @@ int mingw_fstat(int fd, struct stat *buf);

int mingw_utime(const char *file_name, const struct utimbuf *times);
#define utime mingw_utime
int mingw_utimensat(int fd, const char *path, const struct timespec times[2], int flag);
#define utimensat mingw_utimensat
size_t mingw_strftime(char *s, size_t max,
const char *format, const struct tm *tm);
#define strftime mingw_strftime

View File

@ -1391,22 +1391,33 @@ int mingw_fstat(int fd, struct stat *buf)
}
}

static inline void time_t_to_filetime(time_t t, FILETIME *ft)
static inline void timespec_to_filetime(const struct timespec *ts, FILETIME *ft)
{
long long winTime = t * 10000000LL + 116444736000000000LL;
long long winTime = (long long)ts->tv_sec * 10000000LL + (ts->tv_nsec / 100) + 116444736000000000LL;
ft->dwLowDateTime = winTime;
ft->dwHighDateTime = winTime >> 32;
}

int mingw_utime (const char *file_name, const struct utimbuf *times)
int mingw_utimensat(int fd, const char *path, const struct timespec times[2], int flag)
{
FILETIME mft, aft;
FILETIME *paft = &aft, *pmft = &mft;
int rc;
DWORD attrs;
wchar_t wfilename[MAX_PATH];
HANDLE osfilehandle;

if (xutftowcs_path(wfilename, file_name) < 0)
if (fd != AT_FDCWD) {
errno = ENOSYS;
return -1;
}

if (flag) {
errno = ENOSYS;
return -1;
}

if (xutftowcs_path(wfilename, path) < 0)
return -1;

/* must have write permission */
@ -1433,14 +1444,25 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
}

if (times) {
time_t_to_filetime(times->modtime, &mft);
time_t_to_filetime(times->actime, &aft);
if (times[0].tv_nsec == UTIME_NOW)
GetSystemTimeAsFileTime(&aft);
else if (times[0].tv_nsec == UTIME_OMIT)
paft = NULL;
else
timespec_to_filetime(&times[0], &aft);

if (times[1].tv_nsec == UTIME_NOW)
GetSystemTimeAsFileTime(&mft);
else if (times[1].tv_nsec == UTIME_OMIT)
pmft = NULL;
else
timespec_to_filetime(&times[1], &mft);
} else {
GetSystemTimeAsFileTime(&mft);
aft = mft;
}

if (!SetFileTime(osfilehandle, NULL, &aft, &mft)) {
if (!SetFileTime(osfilehandle, NULL, paft, pmft)) {
errno = EINVAL;
rc = -1;
} else
@ -1458,6 +1480,22 @@ revert_attrs:
return rc;
}

int mingw_utime(const char *file_name, const struct utimbuf *times)
{
struct timespec ts[2];
struct timespec *tsp = NULL;

if (times) {
ts[0].tv_sec = times->actime;
ts[0].tv_nsec = 0;
ts[1].tv_sec = times->modtime;
ts[1].tv_nsec = 0;
tsp = ts;
}

return mingw_utimensat(AT_FDCWD, file_name, tsp, 0);
}

#undef strftime
size_t mingw_strftime(char *s, size_t max,
const char *format, const struct tm *tm)

View File

@ -348,6 +348,24 @@ struct git_iovec {
ssize_t git_writev(int fd, const struct iovec *iov, int iovcnt);
#endif

#ifndef AT_FDCWD
#define AT_FDCWD (-100)
#endif
#ifndef UTIME_NOW
#define UTIME_NOW ((1L << 30) - 1L)
#endif
#ifndef UTIME_OMIT
#define UTIME_OMIT ((1L << 30) - 2L)
#endif

#ifdef NO_UTIMENSAT
#ifdef utimensat
#undef utimensat
#endif
#define utimensat git_utimensat
int git_utimensat(int fd, const char *path, const struct timespec times[2], int flag);
#endif

#ifdef NO_SETENV
#define setenv gitsetenv
int gitsetenv(const char *, const char *, int);
@ -502,13 +520,16 @@ int git_qsort_s(void *base, size_t nmemb, size_t size,

#ifdef NO_NSEC
#undef USE_NSEC
#define ST_ATIME_NSEC(st) 0
#define ST_CTIME_NSEC(st) 0
#define ST_MTIME_NSEC(st) 0
#else
#ifdef USE_ST_TIMESPEC
#define ST_ATIME_NSEC(st) ((unsigned int)((st).st_atimespec.tv_nsec))
#define ST_CTIME_NSEC(st) ((unsigned int)((st).st_ctimespec.tv_nsec))
#define ST_MTIME_NSEC(st) ((unsigned int)((st).st_mtimespec.tv_nsec))
#else
#define ST_ATIME_NSEC(st) ((unsigned int)((st).st_atim.tv_nsec))
#define ST_CTIME_NSEC(st) ((unsigned int)((st).st_ctim.tv_nsec))
#define ST_MTIME_NSEC(st) ((unsigned int)((st).st_mtim.tv_nsec))
#endif

39
compat/utimensat.c Normal file
View File

@ -0,0 +1,39 @@
#include "../git-compat-util.h"

int git_utimensat(int fd, const char *path, const struct timespec times[2], int flag)
{
struct timeval tv[2];
struct timeval *tvp = NULL;

if (fd != AT_FDCWD) {
errno = ENOSYS;
return -1;
}

if (flag) {
errno = ENOSYS;
return -1;
}

if (times) {
for (int i = 0; i < 2; i++) {
if (times[i].tv_nsec == UTIME_NOW) {
struct timeval now;
gettimeofday(&now, NULL);
tv[i] = now;
} else if (times[i].tv_nsec == UTIME_OMIT) {
struct stat st;
if (stat(path, &st) < 0)
return -1;
tv[i].tv_sec = (i == 0) ? st.st_atime : st.st_mtime;
tv[i].tv_usec = (i == 0) ? ST_ATIME_NSEC(st) / 1000 : ST_MTIME_NSEC(st) / 1000;
} else {
tv[i].tv_sec = times[i].tv_sec;
tv[i].tv_usec = times[i].tv_nsec / 1000;
}
}
tvp = tv;
}

return utimes(path, tvp);
}

View File

@ -1146,6 +1146,12 @@ GIT_CHECK_FUNC(mkdtemp,
[NO_MKDTEMP=YesPlease])
GIT_CONF_SUBST([NO_MKDTEMP])
#
# Define NO_UTIMENSAT if you don't have utimensat in the C library.
GIT_CHECK_FUNC(utimensat,
[NO_UTIMENSAT=],
[NO_UTIMENSAT=YesPlease])
GIT_CONF_SUBST([NO_UTIMENSAT])
#
# Define NO_INITGROUPS if you don't have initgroups in the C library.
GIT_CHECK_FUNC(initgroups,
[NO_INITGROUPS=],

View File

@ -380,9 +380,9 @@ set(function_checks
strcasestr memmem strlcpy strtoimax strtoumax strtoull
setenv mkdtemp poll pread memmem writev)

#unsetenv,hstrerror are incompatible with windows build
#unsetenv,hstrerror,utimensat are incompatible with windows build (provided by compat/mingw.c)
if(NOT WIN32)
list(APPEND function_checks unsetenv hstrerror)
list(APPEND function_checks unsetenv hstrerror utimensat)
endif()

foreach(f ${function_checks})
@ -428,6 +428,10 @@ if(NOT HAVE_WRITEV)
endif()

if(NOT WIN32)
if(NOT HAVE_UTIMENSAT)
list(APPEND compat_SOURCES compat/utimensat.c)
endif()

if(NOT HAVE_UNSETENV)
list(APPEND compat_SOURCES compat/unsetenv.c)
endif()

View File

@ -1475,6 +1475,8 @@ else
'unsetenv' : ['unsetenv.c'],
# provided by compat/mingw.c.
'getpagesize' : [],
# provided by compat/mingw.c.
'utimensat' : ['utimensat.c'],
}

if get_option('b_sanitize').contains('address') or get_option('b_sanitize').contains('leak')