[PATCH 1/3] compat/posix: introduce utimensat(2) wrapper

"Alexey Samsonov via GitGitGadget" <[email protected]>
Newsgroups org.kernel.vger.git
Message-ID <13aa80bc0167aae05498bac1c59846274ee00e9d.1787322203.git.gitgitgadget@gmail.com>
From: Alexey Samsonov <[email protected]>

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 <[email protected]>
---
 Makefile                            |  6 ++++
 compat/mingw-posix.h                |  2 ++
 compat/mingw.c                      | 52 +++++++++++++++++++++++++----
 compat/posix.h                      | 21 ++++++++++++
 compat/utimensat.c                  | 39 ++++++++++++++++++++++
 configure.ac                        |  6 ++++
 contrib/buildsystems/CMakeLists.txt |  8 +++--
 meson.build                         |  2 ++
 8 files changed, 127 insertions(+), 9 deletions(-)
 create mode 100644 compat/utimensat.c

diff --git a/Makefile b/Makefile
index d4b775953d..64909d48b2 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/compat/mingw-posix.h b/compat/mingw-posix.h
index 2d989fd762..aab91d76db 100644
--- a/compat/mingw-posix.h
+++ b/compat/mingw-posix.h
@@ -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
diff --git a/compat/mingw.c b/compat/mingw.c
index 4c2f26d454..d09a976191 100644
--- a/compat/mingw.c
+++ b/compat/mingw.c
@@ -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)
diff --git a/compat/posix.h b/compat/posix.h
index 71cc731620..3cac1751aa 100644
--- a/compat/posix.h
+++ b/compat/posix.h
@@ -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
diff --git a/compat/utimensat.c b/compat/utimensat.c
new file mode 100644
index 0000000000..e4c8e8d0b6
--- /dev/null
+++ b/compat/utimensat.c
@@ -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);
+}
diff --git a/configure.ac b/configure.ac
index cfb50112bf..a37a53f5b5 100644
--- a/configure.ac
+++ b/configure.ac
@@ -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=],
diff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt
index 8f56203f34..bb1d96802d 100644
--- a/contrib/buildsystems/CMakeLists.txt
+++ b/contrib/buildsystems/CMakeLists.txt
@@ -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()
diff --git a/meson.build b/meson.build
index d86f2acd2b..a98f63a46c 100644
--- a/meson.build
+++ b/meson.build
@@ -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')
-- 
gitgitgadget
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.