diff --git a/ChangeLog.md b/ChangeLog.md index f83a311..6844aee 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -7,7 +7,16 @@ All notable changes to the project are documented in this file. [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 +- 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 umask is more restrictive - Fix `mkpath()`, and `fmkpath()`, not applying the given mode when the diff --git a/doc/API.md b/doc/API.md index e28102a..7cc82d9 100644 --- a/doc/API.md +++ b/doc/API.md @@ -244,6 +244,16 @@ Generic Functions - `LITE_FOPT_RSYNC_DELETE (0x01)` - `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 `delete`, it is now called `opt`. The APIs are 100% compatible if the diff --git a/src/copyfile.c b/src/copyfile.c index 2936daa..593f1a9 100644 --- a/src/copyfile.c +++ b/src/copyfile.c @@ -157,7 +157,13 @@ ssize_t copyfile(const char *src, const char *dst, int len, int opt) if (!buffer) 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 */ 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. */ isdir = adjust_target(src, &dest); - /* Check if the source file is a symlink ... */ - if (stat(src, &st)) { - size = -1; - goto exit; - } - - /* ... only try readlink() if symlink and @sym is set. */ + /* Only try readlink() if symlink and @sym is set. */ if (sym && (st.st_mode & S_IFMT) == S_IFLNK) { size = readlink(src, buffer, BUFSIZ); if (size > 0) { @@ -181,8 +181,16 @@ ssize_t copyfile(const char *src, const char *dst, int len, int opt) } else { buffer[size] = 0; 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 * dangerous race condition, see CWE-367. */ diff --git a/src/lite.h b/src/lite.h index 2d03290..bf704e4 100644 --- a/src/lite.h +++ b/src/lite.h @@ -67,6 +67,7 @@ extern "C" #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_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 sdbuf sdbuf_t; /**< Opqaue context struct for telnet APIs */ diff --git a/src/rsync.c b/src/rsync.c index 695fac3..9aea73e 100644 --- a/src/rsync.c +++ b/src/rsync.c @@ -24,6 +24,7 @@ #include #include /* AT_* macros */ +#include /* dirname() */ #include /* NULL, free() */ #include /* strlen() */ #include /* rindex() */ @@ -35,7 +36,7 @@ #include "lite.h" 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 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 * @param src Source 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. * * 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_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 * 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)) return 1; - if (copy(src, dst, keep_mtim)) + if (copy(src, dst, opt)) return 1; return 0; @@ -104,7 +117,7 @@ int rsync(char *src, char *dst, int opt, int (*filter)(const char *file)) if (stat(src, &st)) return 1; - if (mdir(dest, sizeof(dest), dst, ptr, &st)) + if (mdir(dest, sizeof(dest), dst, ptr, &st, opt)) return 1; dst = dest; do_mtim = keep_mtim; @@ -124,7 +137,7 @@ int rsync(char *src, char *dst, int opt, int (*filter)(const char *file)) continue; } - if (mdir(dst2, sizeof(dst2), dst, files[i], &sb)) { + if (mdir(dst2, sizeof(dst2), dst, files[i], &sb, opt)) { result++; 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... */ } - if (copy(source, dst, keep_mtim)) + if (copy(source, dst, opt)) result++; } @@ -157,23 +170,91 @@ int rsync(char *src, char *dst, int opt, int (*filter)(const char *file)) 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) { - copyfile(src, dst, 0, opt | LITE_FOPT_COPYFILE_SYM); - if (errno) { - if (errno != EEXIST) - return 1; + char *dest = NULL, *temp = NULL, *file = NULL; + int result = 1, saved_errno, made_temp = 0; + int copt = opt & LITE_FOPT_KEEP_MTIME; - 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. */ -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); + + /* 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 (EEXIST != errno) return 1; diff --git a/test/.gitignore b/test/.gitignore index 7128afe..6701e84 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -5,6 +5,7 @@ /chomp /copyfile /fcopyfile +/copyfile-symlink /dir /fexist /fisdir diff --git a/test/Makefile.am b/test/Makefile.am index ef46bcc..36e1d2c 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -20,6 +20,7 @@ TESTS = TESTS += chomp TESTS += copyfile TESTS += fcopyfile +TESTS += copyfile-symlink TESTS += dir TESTS += fexist TESTS += fisdir @@ -41,3 +42,4 @@ TESTS += which TESTS += yorn check_PROGRAMS = $(TESTS) +copyfile_symlink_SOURCES = copyfile-symlink.c diff --git a/test/copyfile-symlink.c b/test/copyfile-symlink.c new file mode 100644 index 0000000..5310fde --- /dev/null +++ b/test/copyfile-symlink.c @@ -0,0 +1,152 @@ +#include +#include +#include +#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; +}