1- // Copyright © 2023 TTKB, LLC.
1+ // Copyright © 2023-2026 TTKB, LLC.
22//
33// Redistribution and use in source and binary forms, with or without
44// modification, are permitted provided that the following conditions are met:
3636
3737#include <err.h>
3838#include <errno.h>
39+ #include <fcntl.h>
3940#include <libgen.h>
4041#include <stdio.h>
4142#include <stdlib.h>
4445
4546#include "clone.h"
4647
47- int find_zero_file (const char * restrict path ) {
48+ static int find_zero_file (const char * restrict path ) {
4849 if (access (path , W_OK )) {
4950 return 1 ;
5051 }
@@ -97,7 +98,7 @@ char* tmp_name(const char* restrict path, char* restrict out, size_t size) {
9798 return out ;
9899}
99100
100- int genfile_clone (const char * src , const char * dst ) {
101+ static int genfile_clone (const char * src , const char * dst ) {
101102#if defined(__APPLE__ )
102103 return clonefile (src , dst , 0 );
103104#elif defined(__FREEBSD__ )
@@ -129,26 +130,97 @@ int genfile_clone(const char* src, const char* dst) {
129130#endif
130131}
131132
132- int replace_with_clone (const char * src , const char * dst ) {
133+ // get the parent directory mtime and return it and a file descriptor
134+ // for the directory. if return is 0, caller is responsible for closing
135+ // the returned fd
136+ static int dir_mtime (const char * path , int * fd_out , struct timespec * mtime_out ) {
137+ char buffer [PATH_MAX ] = { 0 };
138+ char * parent = dirname_r (path , buffer );
139+ if (!parent ) {
140+ perror ("dirname_r" );
141+ return -1 ;
142+ }
143+
144+ int fd = open (parent , O_RDONLY );
145+ if (fd == -1 ) {
146+ perror ("open parent dir" );
147+ return -1 ;
148+ }
149+
150+ struct stat st ;
151+ if (fstat (fd , & st ) == -1 ) {
152+ perror ("fstat parent dir" );
153+ close (fd );
154+ return -1 ;
155+ }
156+
157+ * fd_out = fd ;
158+ * mtime_out = st .st_mtimespec ;
159+ return 0 ;
160+ }
161+
162+ // restore the provided mtime to the provided fd. fd is closed
163+ // before the function returns
164+ static void restore_dir_mtime (int fd , struct timespec mtime ) {
165+ struct timespec times [2 ] = {
166+ // omit atime
167+ { .tv_sec = 0 , .tv_nsec = UTIME_OMIT },
168+ mtime ,
169+ };
170+ if (futimens (fd , times ) == -1 ) {
171+ switch (errno ) {
172+ case EPERM :
173+ fprintf (stderr , "Warning: cannot preserve parent mtime, permission denied\n" );
174+ break ;
175+ case EROFS :
176+ fprintf (stderr , "Warning: cannot preserve parent mtime, filesystem is read-only\n" );
177+ break ;
178+ default :
179+ perror ("Warning: futimens" );
180+ break ;
181+ }
182+ }
183+ close (fd );
184+ }
185+
186+ #ifdef DEBUG
187+ #define COPYFILE_DEBUG (1<<31)
188+ #else
189+ #define COPYFILE_DEBUG (0)
190+ #endif
191+
192+ int replace_with_clone (const char * src , const char * dst , bool preserve_parent_mtime ) {
193+ int parent_fd = -1 ;
194+ struct timespec saved_mtime = { 0 };
195+
196+ if (preserve_parent_mtime &&
197+ dir_mtime (dst , & parent_fd , & saved_mtime ) == -1 ) {
198+ return errno ;
199+ }
200+
201+ int result = 0 ;
202+
133203 char path [PATH_MAX ] = { 0 };
134204 if (!tmp_name (dst , path , PATH_MAX )) {
135- return errno ;
205+ result = errno ;
206+ goto cleanup ;
136207 }
137208
138209 errno = 0 ;
139- int result = genfile_clone (src , path );
210+ result = genfile_clone (src , path );
140211
141212 if (result ) {
142213 perror ("could not clonefile" );
143214 unlink (path ); // if it exists
144- return result ;
215+ goto cleanup ;
145216 }
146217
147218 if (find_zero_file (path )) {
148219 fprintf (stderr ,
149220 "invalid file created by clonefile(2)\n" );
150221 unlink (path );
151- return ENOENT ;
222+ result = ENOENT ;
223+ goto cleanup ;
152224 }
153225
154226#if defined(__APPLE__ )
@@ -161,24 +233,26 @@ int replace_with_clone(const char* src, const char* dst) {
161233 if (check & COPYFILE_DATA ) {
162234 perror ("copyfile(3) should not copy data" );
163235 unlink (path );
164- return check ;
236+ result = check ;
237+ goto cleanup ;
165238 }
166239
167240 result = copyfile (dst ,
168241 path ,
169242 NULL ,
170- COPYFILE_METADATA | ( 1 << 31 ) );
243+ COPYFILE_METADATA | COPYFILE_DEBUG );
171244 if (result ) {
172245 perror ("could not copy metadata" );
173246 unlink (path );
174- return result ;
247+ goto cleanup ;
175248 }
176249
177250 if (find_zero_file (path )) {
178251 fprintf (stderr ,
179252 "invalid file created by copyfile(3)\n" );
180253 unlink (path );
181- return ENOENT ;
254+ result = ENOENT ;
255+ goto cleanup ;
182256 }
183257#else
184258#error Operating system not supported
@@ -191,10 +265,14 @@ int replace_with_clone(const char* src, const char* dst) {
191265 if (result ) {
192266 perror ("could not replace existing file" );
193267 unlink (path );
194- return result ;
268+ goto cleanup ;
195269 }
196270
197- return 0 ;
271+ cleanup :
272+ if (preserve_parent_mtime ) {
273+ restore_dir_mtime (parent_fd , saved_mtime );
274+ }
275+ return result ;
198276}
199277
200278
@@ -211,14 +289,25 @@ int replace_with_link(const char* src, const char* dst) {
211289
212290// returns a relative path to dst from src
213291char * path_relative_to (const char * src , const char * dst ) {
214- char * real_src = realpath (src , NULL ),
215- * real_dst = realpath (dst , NULL ),
216- * orig_real_src = real_src ,
292+ char * real_src = realpath (src , NULL );
293+ if (real_src == NULL ) {
294+ fprintf (stderr , "%s could not be resolved to a canonical path.\n" , src );
295+ return NULL ;
296+ }
297+
298+ char * real_dst = realpath (dst , NULL );
299+ if (real_dst == NULL ) {
300+ free (real_src );
301+ fprintf (stderr , "%s could not be resolved to a canonical path.\n" , dst );
302+ return NULL ;
303+ }
304+
305+ char * orig_real_src = real_src ,
217306 * orig_real_dst = real_dst ;
218307
219- // consume root /
220- real_src ++ ;
221- real_dst ++ ;
308+ // consume root '/' if it exists
309+ if ( * real_src == '/' ) real_src ++ ;
310+ if ( * real_dst == '/' ) real_dst ++ ;
222311
223312 int depth = 0 ;
224313
@@ -275,6 +364,8 @@ char* path_relative_to(const char* src, const char* dst) {
275364}
276365
277366int replace_with_symlink (const char * src , const char * dst ) {
367+ // must be called prior to unlink because realpath
368+ // is used. unlinking first removes the real path
278369 char * path = path_relative_to (dst , src );
279370
280371 // TODO: should this atomically move a tmp file instead of
0 commit comments