From d83b2e9f51539287d5d2b1bd939ea9540fa6e5be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Sat, 5 Sep 2015 03:54:06 +0200 Subject: [PATCH] filebuf: follow symlinks when creating a lock file We create a lockfile to update files under GIT_DIR. Sometimes these files are actually located elsewhere and a symlink takes their place. In that case we should lock and update the file at its final location rather than overwrite the symlink. --- CHANGELOG.md | 3 ++ src/filebuf.c | 84 ++++++++++++++++++++++++++++++++++++++++++-- tests/core/filebuf.c | 53 ++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f28cbf3b..1f3c3ed6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ v0.23 + 1 example `filter=*`. Consumers should examine the attributes parameter of the `check` function for details. +* Symlinks are now followed when locking a file, which can be + necessary when multiple worktrees share a base repository. + ### API additions * `git_config_lock()` has been added, which allow for diff --git a/src/filebuf.c b/src/filebuf.c index 838f4b4d2..2bbc210ba 100644 --- a/src/filebuf.c +++ b/src/filebuf.c @@ -191,6 +191,81 @@ static int write_deflate(git_filebuf *file, void *source, size_t len) return 0; } +#define MAX_SYMLINK_DEPTH 5 + +static int resolve_symlink(git_buf *out, const char *path) +{ + int i, error, root; + ssize_t ret; + struct stat st; + git_buf curpath = GIT_BUF_INIT, target = GIT_BUF_INIT; + + if ((error = git_buf_grow(&target, GIT_PATH_MAX + 1)) < 0 || + (error = git_buf_puts(&curpath, path)) < 0) + return error; + + for (i = 0; i < MAX_SYMLINK_DEPTH; i++) { + error = p_lstat(curpath.ptr, &st); + if (error < 0 && errno == ENOENT) { + error = git_buf_puts(out, curpath.ptr); + goto cleanup; + } + + if (error < 0) { + giterr_set(GITERR_OS, "failed to stat '%s'", curpath.ptr); + error = -1; + goto cleanup; + } + + if (!S_ISLNK(st.st_mode)) { + error = git_buf_puts(out, curpath.ptr); + goto cleanup; + } + + ret = p_readlink(curpath.ptr, target.ptr, GIT_PATH_MAX); + if (ret < 0) { + giterr_set(GITERR_OS, "failed to read symlink '%s'", curpath.ptr); + error = -1; + goto cleanup; + } + + if (ret == GIT_PATH_MAX) { + giterr_set(GITERR_INVALID, "symlink target too long"); + error = -1; + goto cleanup; + } + + /* readlink(2) won't NUL-terminate for us */ + target.ptr[ret] = '\0'; + target.size = ret; + + root = git_path_root(target.ptr); + if (root >= 0) { + if ((error = git_buf_puts(&curpath, target.ptr)) < 0) + goto cleanup; + } else { + git_buf dir = GIT_BUF_INIT; + + if ((error = git_path_dirname_r(&dir, curpath.ptr)) < 0) + goto cleanup; + + git_buf_swap(&curpath, &dir); + git_buf_free(&dir); + + if ((error = git_path_apply_relative(&curpath, target.ptr)) < 0) + goto cleanup; + } + } + + giterr_set(GITERR_INVALID, "maximum symlink depth reached"); + error = -1; + +cleanup: + git_buf_free(&curpath); + git_buf_free(&target); + return error; +} + int git_filebuf_open(git_filebuf *file, const char *path, int flags, mode_t mode) { int compression, error = -1; @@ -265,11 +340,14 @@ int git_filebuf_open(git_filebuf *file, const char *path, int flags, mode_t mode file->path_lock = git_buf_detach(&tmp_path); GITERR_CHECK_ALLOC(file->path_lock); } else { - path_len = strlen(path); + git_buf resolved_path = GIT_BUF_INIT; + + if ((error = resolve_symlink(&resolved_path, path)) < 0) + goto cleanup; /* Save the original path of the file */ - file->path_original = git__strdup(path); - GITERR_CHECK_ALLOC(file->path_original); + path_len = resolved_path.size; + file->path_original = git_buf_detach(&resolved_path); /* create the locking path by appending ".lock" to the original */ GITERR_CHECK_ALLOC_ADD(&alloc_len, path_len, GIT_FILELOCK_EXTLENGTH); diff --git a/tests/core/filebuf.c b/tests/core/filebuf.c index 3f7dc8569..39d98ff7e 100644 --- a/tests/core/filebuf.c +++ b/tests/core/filebuf.c @@ -151,3 +151,56 @@ void test_core_filebuf__rename_error(void) cl_assert_equal_i(false, git_path_exists(test_lock)); } + +void test_core_filebuf__symlink_follow(void) +{ + git_filebuf file = GIT_FILEBUF_INIT; + const char *dir = "linkdir", *source = "linkdir/link"; + +#ifdef GIT_WIN32 + cl_skip(); +#endif + + cl_git_pass(p_mkdir(dir, 0777)); + cl_git_pass(p_symlink("target", source)); + + cl_git_pass(git_filebuf_open(&file, source, 0, 0666)); + cl_git_pass(git_filebuf_printf(&file, "%s\n", "libgit2 rocks")); + + cl_assert_equal_i(true, git_path_exists("linkdir/target.lock")); + + cl_git_pass(git_filebuf_commit(&file)); + cl_assert_equal_i(true, git_path_exists("linkdir/target")); + + git_filebuf_cleanup(&file); + + /* The second time around, the target file does exist */ + cl_git_pass(git_filebuf_open(&file, source, 0, 0666)); + cl_git_pass(git_filebuf_printf(&file, "%s\n", "libgit2 rocks")); + + cl_assert_equal_i(true, git_path_exists("linkdir/target.lock")); + + cl_git_pass(git_filebuf_commit(&file)); + cl_assert_equal_i(true, git_path_exists("linkdir/target")); + + git_filebuf_cleanup(&file); + cl_git_pass(git_futils_rmdir_r(dir, NULL, GIT_RMDIR_REMOVE_FILES)); +} + +void test_core_filebuf__symlink_depth(void) +{ + git_filebuf file = GIT_FILEBUF_INIT; + const char *dir = "linkdir", *source = "linkdir/link"; + +#ifdef GIT_WIN32 + cl_skip(); +#endif + + cl_git_pass(p_mkdir(dir, 0777)); + /* Endless loop */ + cl_git_pass(p_symlink("link", source)); + + cl_git_fail(git_filebuf_open(&file, source, 0, 0666)); + + cl_git_pass(git_futils_rmdir_r(dir, NULL, GIT_RMDIR_REMOVE_FILES)); +}