Merge branch 'copyfile-symlinks'

Closes #23

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg committed 2026-11-14 12:49:17 +01:00
commit 798d70e2a2
8 files changed
+286 -22

No files matched your search

+9
View File
@@ -7,7 +7,16 @@ All notable changes to the project are documented in this file.
[UNRELEASED][] [UNRELEASED][]
-------------- --------------
### Changes
- Add `LITE_FOPT_RSYNC_LINKS` option to `rsync()`, recreates symlinks to
files in the destination instead of copying what they point to
### Fixes ### Fixes
- Fix #23: `copyfile()` ignored `LITE_FOPT_COPYFILE_SYM` and always
copied what a symlink points to. It now recreates the symlink, also
when it is dangling or points to a directory
- Fix #22: `fcopyfile()` truncated binary data at the first NUL byte in
each chunk, and returned success on read and write errors
- Fix `copyfile()` not preserving the source file mode when the process - Fix `copyfile()` not preserving the source file mode when the process
umask is more restrictive umask is more restrictive
- Fix `mkpath()`, and `fmkpath()`, not applying the given mode when the - Fix `mkpath()`, and `fmkpath()`, not applying the given mode when the
+10
View File
@@ -244,6 +244,16 @@ Generic Functions
- `LITE_FOPT_RSYNC_DELETE (0x01)` - `LITE_FOPT_RSYNC_DELETE (0x01)`
- `LITE_FOPT_KEEP_MTIME (0x02)` - `LITE_FOPT_KEEP_MTIME (0x02)`
- `LITE_FOPT_RSYNC_LINKS (0x04)`
Symlinks in the source tree are followed by default, the destination
gets a copy of what they point to. With `LITE_FOPT_RSYNC_LINKS` a
symlink to a file, or a dangling symlink, is recreated in the
destination instead. Symlinks to directories are always followed.
With `LITE_FOPT_RSYNC_LINKS` a symlink in the destination is never
followed. If one sits where a file or directory from the source
goes, it is removed first.
In releases prior to v2.0 the argument controlling pruning was called In releases prior to v2.0 the argument controlling pruning was called
`delete`, it is now called `opt`. The APIs are 100% compatible if the `delete`, it is now called `opt`. The APIs are 100% compatible if the
+17 -9
View File
@@ -157,7 +157,13 @@ ssize_t copyfile(const char *src, const char *dst, int len, int opt)
if (!buffer) if (!buffer)
return 0; return 0;
if (fisdir(src)) { if (sym ? lstat(src, &st) : stat(src, &st)) {
saved_errno = errno;
size = -1;
goto exit;
}
if (S_ISDIR(st.st_mode)) {
saved_errno = EISDIR; /* Error: source is a directory */ saved_errno = EISDIR; /* Error: source is a directory */
goto exit; goto exit;
} }
@@ -165,13 +171,7 @@ ssize_t copyfile(const char *src, const char *dst, int len, int opt)
/* Check if target is a directory, then append src filename. */ /* Check if target is a directory, then append src filename. */
isdir = adjust_target(src, &dest); isdir = adjust_target(src, &dest);
/* Check if the source file is a symlink ... */ /* Only try readlink() if symlink and @sym is set. */
if (stat(src, &st)) {
size = -1;
goto exit;
}
/* ... only try readlink() if symlink and @sym is set. */
if (sym && (st.st_mode & S_IFMT) == S_IFLNK) { if (sym && (st.st_mode & S_IFMT) == S_IFLNK) {
size = readlink(src, buffer, BUFSIZ); size = readlink(src, buffer, BUFSIZ);
if (size > 0) { if (size > 0) {
@@ -181,8 +181,16 @@ ssize_t copyfile(const char *src, const char *dst, int len, int opt)
} else { } else {
buffer[size] = 0; buffer[size] = 0;
size = !symlink(buffer, dest); size = !symlink(buffer, dest);
if (!size)
saved_errno = errno;
else if (keep_mtim) {
struct timespec tv[2] = { st.st_atim, st.st_mtim };
utimensat(AT_FDCWD, dest, tv, AT_SYMLINK_NOFOLLOW);
}
} }
} } else
saved_errno = errno;
/* Don't fall back to copy, that is a potentially /* Don't fall back to copy, that is a potentially
* dangerous race condition, see CWE-367. */ * dangerous race condition, see CWE-367. */
+1
View File
@@ -67,6 +67,7 @@ extern "C"
#define LITE_FOPT_RSYNC_DELETE 0x01 /**< Prune files from destination that are not in source */ #define LITE_FOPT_RSYNC_DELETE 0x01 /**< Prune files from destination that are not in source */
#define LITE_FOPT_COPYFILE_SYM 0x01 /**< Recreate symlink or follow to copy target */ #define LITE_FOPT_COPYFILE_SYM 0x01 /**< Recreate symlink or follow to copy target */
#define LITE_FOPT_KEEP_MTIME 0x02 /**< Preserve modification time */ #define LITE_FOPT_KEEP_MTIME 0x02 /**< Preserve modification time */
#define LITE_FOPT_RSYNC_LINKS 0x04 /**< Recreate symlinks to files instead of copying their target */
typedef struct lfile lfile_t; /**< Opqaue context struct for lfile APIs */ typedef struct lfile lfile_t; /**< Opqaue context struct for lfile APIs */
typedef struct sdbuf sdbuf_t; /**< Opqaue context struct for telnet APIs */ typedef struct sdbuf sdbuf_t; /**< Opqaue context struct for telnet APIs */
+94 -13
View File
@@ -24,6 +24,7 @@
#include <errno.h> #include <errno.h>
#include <fcntl.h> /* AT_* macros */ #include <fcntl.h> /* AT_* macros */
#include <libgen.h> /* dirname() */
#include <stdlib.h> /* NULL, free() */ #include <stdlib.h> /* NULL, free() */
#include <string.h> /* strlen() */ #include <string.h> /* strlen() */
#include <strings.h> /* rindex() */ #include <strings.h> /* rindex() */
@@ -35,7 +36,7 @@
#include "lite.h" #include "lite.h"
static int copy(char *src, char *dst, int opt); static int copy(char *src, char *dst, int opt);
static int mdir(char *buf, size_t buf_len, char *dir, char *name, struct stat *st); static int mdir(char *buf, size_t buf_len, char *dir, char *name, struct stat *st, int opt);
static int prune(char *dst, char **new_files, int new_num); static int prune(char *dst, char **new_files, int new_num);
static int set_mtime(char *fn, struct stat *st); static int set_mtime(char *fn, struct stat *st);
@@ -44,7 +45,8 @@ static int set_mtime(char *fn, struct stat *st);
* Synchronize contents and optionally remove non-existing backups * Synchronize contents and optionally remove non-existing backups
* @param src Source directory * @param src Source directory
* @param dst Destination directory * @param dst Destination directory
* @param opt An option mask of ::LITE_FOPT_RSYNC_DELETE, ::LITE_FOPT_KEEP_MTIME * @param opt An option mask of ::LITE_FOPT_RSYNC_DELETE, ::LITE_FOPT_KEEP_MTIME,
* ::LITE_FOPT_RSYNC_LINKS
* @param filter Optional filtering function for source directory. * @param filter Optional filtering function for source directory.
* *
* This is a miniature implementation of the famous rsync for local use * This is a miniature implementation of the famous rsync for local use
@@ -59,6 +61,17 @@ static int set_mtime(char *fn, struct stat *st);
* *
* %LITE_FOPT_RSYNC_DELETE: Prune files from @p dst that no longer exist in @p src. * %LITE_FOPT_RSYNC_DELETE: Prune files from @p dst that no longer exist in @p src.
* %LITE_FOPT_KEEP_MTIME: Preserve modification time * %LITE_FOPT_KEEP_MTIME: Preserve modification time
* %LITE_FOPT_RSYNC_LINKS: Recreate symlinks to files in @p dst
*
* Symlinks in @p src are followed by default, so @p dst gets a copy of
* what they point to. With ::LITE_FOPT_RSYNC_LINKS a symlink to a file,
* or a dangling symlink, is recreated in @p dst with the same target
* text, replacing any file or symlink already there. Symlinks to
* directories in @p src are always followed.
*
* With ::LITE_FOPT_RSYNC_LINKS a symlink in @p dst is never followed.
* If one sits where a file or directory from @p src goes, it is removed
* first.
* *
* The filter callback, @p filter, if provided, is used to determine * The filter callback, @p filter, if provided, is used to determine
* what files to include from the source directory when backing up. If * what files to include from the source directory when backing up. If
@@ -86,7 +99,7 @@ int rsync(char *src, char *dst, int opt, int (*filter)(const char *file))
if (!fexist(src)) if (!fexist(src))
return 1; return 1;
if (copy(src, dst, keep_mtim)) if (copy(src, dst, opt))
return 1; return 1;
return 0; return 0;
@@ -104,7 +117,7 @@ int rsync(char *src, char *dst, int opt, int (*filter)(const char *file))
if (stat(src, &st)) if (stat(src, &st))
return 1; return 1;
if (mdir(dest, sizeof(dest), dst, ptr, &st)) if (mdir(dest, sizeof(dest), dst, ptr, &st, opt))
return 1; return 1;
dst = dest; dst = dest;
do_mtim = keep_mtim; do_mtim = keep_mtim;
@@ -124,7 +137,7 @@ int rsync(char *src, char *dst, int opt, int (*filter)(const char *file))
continue; continue;
} }
if (mdir(dst2, sizeof(dst2), dst, files[i], &sb)) { if (mdir(dst2, sizeof(dst2), dst, files[i], &sb, opt)) {
result++; result++;
continue; continue;
} }
@@ -136,7 +149,7 @@ int rsync(char *src, char *dst, int opt, int (*filter)(const char *file))
continue; /* Next file/dir in @src to copy... */ continue; /* Next file/dir in @src to copy... */
} }
if (copy(source, dst, keep_mtim)) if (copy(source, dst, opt))
result++; result++;
} }
@@ -157,23 +170,91 @@ int rsync(char *src, char *dst, int opt, int (*filter)(const char *file))
return result; return result;
} }
/* Leaves errno alone, a missing @path is not an error to rsync() */
static int islink(const char *path)
{
int rc, saved_errno = errno;
struct stat st;
rc = !lstat(path, &st) && S_ISLNK(st.st_mode);
errno = saved_errno;
return rc;
}
static int copy(char *src, char *dst, int opt) static int copy(char *src, char *dst, int opt)
{ {
copyfile(src, dst, 0, opt | LITE_FOPT_COPYFILE_SYM); char *dest = NULL, *temp = NULL, *file = NULL;
if (errno) { int result = 1, saved_errno, made_temp = 0;
if (errno != EEXIST) int copt = opt & LITE_FOPT_KEEP_MTIME;
return 1;
errno = 0; if (!(opt & LITE_FOPT_RSYNC_LINKS)) {
copyfile(src, dst, 0, copt);
return errno ? 1 : 0;
}
copt |= LITE_FOPT_COPYFILE_SYM;
if (fisdir(dst)) {
char *base = strrchr(src, '/');
if (asprintf(&dest, "%s%s%s", dst, fisslashdir(dst) ? "" : "/", base ? base + 1 : src) < 0)
return 1;
} else {
dest = strdup(dst);
if (!dest)
return 1;
} }
return 0; /*
* A link in the destination, e.g. from a previous run, may point
* anywhere. Remove it so copyfile() cannot write through it.
*/
if (islink(dest) && !islink(src))
unlink(dest);
copyfile(src, dst, 0, copt);
if (errno != EEXIST) {
result = errno ? 1 : 0;
goto exit;
}
/* Replace an existing destination only after the new link is ready. */
if (asprintf(&temp, "%s/.rsync-XXXXXX", dirname(strdupa(dest))) < 0)
goto exit;
if (!mkdtemp(temp))
goto exit;
made_temp = 1;
if (asprintf(&file, "%s/link", temp) < 0)
goto exit;
copyfile(src, file, 0, copt);
if (errno)
goto exit;
result = rename(file, dest) != 0;
exit:
saved_errno = errno;
if (file)
unlink(file);
if (made_temp)
rmdir(temp);
free(file);
free(temp);
free(dest);
errno = saved_errno;
return result;
} }
/* Creates dir/name @mode ... skipping / if dir already ends so. */ /* Creates dir/name @mode ... skipping / if dir already ends so. */
static int mdir(char *buf, size_t buf_len, char *dir, char *name, struct stat *st) static int mdir(char *buf, size_t buf_len, char *dir, char *name, struct stat *st, int opt)
{ {
snprintf(buf, buf_len, "%s%s%s", dir, fisslashdir(dir) ? "" : "/", name); snprintf(buf, buf_len, "%s%s%s", dir, fisslashdir(dir) ? "" : "/", name);
/* Same as in copy(), do not descend into a stale link. */
if ((opt & LITE_FOPT_RSYNC_LINKS) && islink(buf))
unlink(buf);
if (mkdir(buf, st->st_mode)) { if (mkdir(buf, st->st_mode)) {
if (EEXIST != errno) if (EEXIST != errno)
return 1; return 1;
+1
View File
@@ -5,6 +5,7 @@
/chomp /chomp
/copyfile /copyfile
/fcopyfile /fcopyfile
/copyfile-symlink
/dir /dir
/fexist /fexist
/fisdir /fisdir
+2
View File
@@ -20,6 +20,7 @@ TESTS =
TESTS += chomp TESTS += chomp
TESTS += copyfile TESTS += copyfile
TESTS += fcopyfile TESTS += fcopyfile
TESTS += copyfile-symlink
TESTS += dir TESTS += dir
TESTS += fexist TESTS += fexist
TESTS += fisdir TESTS += fisdir
@@ -41,3 +42,4 @@ TESTS += which
TESTS += yorn TESTS += yorn
check_PROGRAMS = $(TESTS) check_PROGRAMS = $(TESTS)
copyfile_symlink_SOURCES = copyfile-symlink.c
+152
View File
@@ -0,0 +1,152 @@
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include "check.h"
static void check_link(const char *path, const char *target)
{
char buf[256];
struct stat st;
ssize_t len;
fail_unless(lstat(path, &st) == 0 && S_ISLNK(st.st_mode));
len = readlink(path, buf, sizeof(buf));
fail_unless(len == (ssize_t)strlen(target));
fail_unless(memcmp(buf, target, len) == 0);
}
/* A link left in the destination by an earlier run must never be written through. */
static void rsync_stale_links(void)
{
struct stat st;
FILE *fp;
fail_unless(mkdir("src", 0700) == 0 && mkdir("dst", 0700) == 0);
fail_unless(touch("victim") == 0);
fail_unless(symlink("../victim", "src/a") == 0);
fail_unless(symlink("../victims", "src/d") == 0);
fail_unless(rsync("src/", "dst/", LITE_FOPT_RSYNC_LINKS, NULL) == 0);
check_link("dst/a", "../victim");
check_link("dst/d", "../victims");
/* The source links become a file and a directory, and dst/d stops dangling. */
fail_unless(unlink("src/a") == 0 && unlink("src/d") == 0);
fp = fopen("src/a", "w");
fail_unless(fp && fputs("data", fp) >= 0 && fclose(fp) == 0);
fail_unless(mkdir("src/d", 0700) == 0 && touch("src/d/f") == 0);
fail_unless(mkdir("victims", 0700) == 0);
fail_unless(rsync("src/", "dst/", LITE_FOPT_RSYNC_LINKS, NULL) == 0);
fail_unless(lstat("dst/a", &st) == 0 && S_ISREG(st.st_mode) && st.st_size == 4);
fail_unless(stat("victim", &st) == 0 && st.st_size == 0);
fail_unless(lstat("dst/d", &st) == 0 && S_ISDIR(st.st_mode));
fail_unless(fexist("dst/d/f") && !fexist("victims/f"));
fail_unless(unlink("src/a") == 0 && unlink("src/d/f") == 0 && rmdir("src/d") == 0);
fail_unless(unlink("dst/a") == 0 && unlink("dst/d/f") == 0 && rmdir("dst/d") == 0);
fail_unless(rmdir("src") == 0 && rmdir("dst") == 0);
fail_unless(unlink("victim") == 0 && rmdir("victims") == 0);
}
int main(void)
{
char tmp[] = "/tmp/libite-copylink-XXXXXX";
const char payload[] = "target contents";
char buf[sizeof(payload)];
const char *targets[] = { "file", "dir", "missing", ".", "link" };
struct timespec times[2] = { { 1234567800, 0 }, { 1234567801, 0 } };
struct stat src_st, dst_st;
char cwd[4096];
size_t i;
FILE *fp;
fail_unless(getcwd(cwd, sizeof(cwd)) && mkdtemp(tmp));
fail_unless(chdir(tmp) == 0);
fail_unless(mkdir("dir", 0700) == 0);
fail_unless(mkdir("out", 0700) == 0);
fp = fopen("file", "w");
fail_unless(fp && fwrite(payload, 1, sizeof(payload), fp) == sizeof(payload));
fail_unless(fclose(fp) == 0);
for (i = 0; i < NELEMS(targets); i++) {
fail_unless(symlink(targets[i], "link") == 0);
fail_unless(copyfile("link", "copied", 0, LITE_FOPT_COPYFILE_SYM) == 1);
check_link("copied", targets[i]);
fail_unless(copyfile("link", "out", 0, LITE_FOPT_COPYFILE_SYM) == 1);
check_link("out/link", targets[i]);
/* Existing targets must survive a failed symlink creation. */
errno = 0;
fail_unless(copyfile("link", "file", 0, LITE_FOPT_COPYFILE_SYM) == 0);
fail_unless(errno == EEXIST);
check_link("copied", targets[i]);
fail_unless(unlink("copied") == 0 && unlink("out/link") == 0);
fail_unless(unlink("link") == 0);
}
fail_unless(symlink("file", "link") == 0);
/* Without SYM, continue to copy target data and preserve regular-file metadata. */
fail_unless(copyfile("link", "copied", 0, LITE_FOPT_KEEP_MTIME) == sizeof(payload));
fail_unless(lstat("copied", &dst_st) == 0 && S_ISREG(dst_st.st_mode));
fail_unless(stat("file", &src_st) == 0 && src_st.st_mtime == dst_st.st_mtime);
fp = fopen("copied", "r");
fail_unless(fp && fread(buf, 1, sizeof(buf), fp) == sizeof(payload));
fail_unless(memcmp(buf, payload, sizeof(payload)) == 0 && fclose(fp) == 0);
fail_unless(unlink("copied") == 0);
fail_unless(utimensat(AT_FDCWD, "link", times, AT_SYMLINK_NOFOLLOW) == 0);
fail_unless(copyfile("link", "copied", 0, LITE_FOPT_COPYFILE_SYM | LITE_FOPT_KEEP_MTIME) == 1);
check_link("copied", "file");
fail_unless(lstat("copied", &dst_st) == 0 && dst_st.st_mtime == times[1].tv_sec);
fail_unless(unlink("copied") == 0);
/* rsync() follows links unless asked to recreate them. */
fail_unless(rsync("link", "copied", 0, NULL) == 0);
fail_unless(lstat("copied", &dst_st) == 0 && S_ISREG(dst_st.st_mode));
fail_unless(dst_st.st_size == sizeof(payload));
fail_unless(unlink("copied") == 0);
fail_unless(rsync("link", "copied", LITE_FOPT_RSYNC_LINKS, NULL) == 0);
check_link("copied", "file");
/* Reuse the destination after changing the source link. */
fail_unless(unlink("link") == 0 && symlink("file", "link") == 0);
fail_unless(rsync("link", "out", LITE_FOPT_RSYNC_LINKS, NULL) == 0);
check_link("out/link", "file");
fail_unless(unlink("link") == 0 && symlink("copied", "link") == 0);
fail_unless(rsync("link", "out", LITE_FOPT_RSYNC_LINKS, NULL) == 0);
check_link("out/link", "copied");
/* A same-path copy must not unlink its source. */
fail_unless(rsync("link", "link", LITE_FOPT_RSYNC_LINKS, NULL) == 0);
check_link("link", "copied");
fail_unless(unlink("out/link") == 0);
/* Replacing a regular destination must also retain the source link. */
fail_unless(touch("out/link") == 0);
fail_unless(rsync("link", "out", LITE_FOPT_RSYNC_LINKS, NULL) == 0);
check_link("out/link", "copied");
fail_unless(unlink("out/link") == 0);
/* A failed replacement must keep both the destination and source. */
fail_unless(mkdir("out/link", 0700) == 0);
fail_unless(touch("out/link/keep") == 0);
fail_unless(rsync("link", "out", LITE_FOPT_RSYNC_LINKS, NULL) != 0);
fail_unless(fexist("out/link/keep"));
check_link("link", "copied");
fail_unless(unlink("out/link/keep") == 0 && rmdir("out/link") == 0);
if (getuid() != 0) {
fail_unless(mkdir("protected", 0700) == 0);
fail_unless(symlink("original", "protected/link") == 0);
fail_unless(chmod("protected", 0500) == 0);
fail_unless(rsync("link", "protected", LITE_FOPT_RSYNC_LINKS, NULL) != 0);
check_link("protected/link", "original");
check_link("link", "copied");
fail_unless(chmod("protected", 0700) == 0);
fail_unless(unlink("protected/link") == 0 && rmdir("protected") == 0);
}
fail_unless(unlink("copied") == 0 && unlink("link") == 0);
rsync_stale_links();
errno = 0;
fail_unless(copyfile("dir", "copied", 0, LITE_FOPT_COPYFILE_SYM) == 0 && errno == EISDIR);
errno = 0;
fail_unless(copyfile("missing", "copied", 0, LITE_FOPT_COPYFILE_SYM) == -1 && errno == ENOENT);
fail_unless(unlink("file") == 0 && rmdir("dir") == 0 && rmdir("out") == 0);
fail_unless(chdir(cwd) == 0 && rmdir(tmp) == 0);
return 0;
}