commit 8ba819a3547825c0e0d657a7e41610da16f6cd4f from: Stefan Sperling date: Thu Jul 23 14:21:27 2020 UTC let 'got checkout' create symlinks in a work tree commit - 377624f7f3328486605f0c0ca78abc398440bdbe commit + 8ba819a3547825c0e0d657a7e41610da16f6cd4f blob - 54c7093a4d0bc68cfd3ef1939b7c80f71106e777 blob + b0dceed9f15dbec807d84c8744cbe9fce22faa76 --- include/got_object.h +++ include/got_object.h @@ -254,6 +254,9 @@ const uint8_t *got_object_blob_get_read_buf(struct got const struct got_error *got_object_blob_read_block(size_t *, struct got_blob_object *); +/* Rewind an open blob's data stream back to the beginning. */ +void got_object_blob_rewind(struct got_blob_object *); + /* * Read the entire content of a blob and write it to the specified file. * Flush and rewind the file as well. Indicate the amount of bytes blob - 9a3beff5204483df08335c4ff3dfe9f49015cf50 blob + 0138ca96e984ed90c59ea99cf3e066a60af6138b --- lib/object.c +++ lib/object.c @@ -1204,6 +1204,13 @@ got_object_blob_close(struct got_blob_object *blob) return err; } +void +got_object_blob_rewind(struct got_blob_object *blob) +{ + if (blob->f) + rewind(blob->f); +} + char * got_object_blob_id_str(struct got_blob_object *blob, char *buf, size_t size) { blob - 985b0d77e0e47f32bdbe60268d8184e7df1a8e47 blob + 1f4dfd3657f769e5b82c587069c4f33f57427be0 --- lib/worktree.c +++ lib/worktree.c @@ -935,18 +935,175 @@ get_ondisk_perms(int executable, mode_t st_mode) return (st_mode & ~(S_IXUSR | S_IXGRP | S_IXOTH)); } +/* forward declaration */ static const struct got_error * install_blob(struct got_worktree *worktree, const char *ondisk_path, const char *path, mode_t te_mode, mode_t st_mode, struct got_blob_object *blob, int restoring_missing_file, int reverting_versioned_file, struct got_repository *repo, + got_worktree_checkout_cb progress_cb, void *progress_arg); + +static const struct got_error * +install_symlink(struct got_worktree *worktree, const char *ondisk_path, + const char *path, mode_t te_mode, mode_t st_mode, + struct got_blob_object *blob, int restoring_missing_file, + int reverting_versioned_file, struct got_repository *repo, got_worktree_checkout_cb progress_cb, void *progress_arg) { const struct got_error *err = NULL; + char target_path[PATH_MAX]; + size_t len, target_len = 0; + char *resolved_path = NULL, *abspath = NULL; + const uint8_t *buf = got_object_blob_get_read_buf(blob); + size_t hdrlen = got_object_blob_get_hdrlen(blob); + + /* + * Blob object content specifies the target path of the link. + * If a symbolic link cannot be installed we instead create + * a regular file which contains the link target path stored + * in the blob object. + */ + do { + err = got_object_blob_read_block(&len, blob); + if (len + target_len >= sizeof(target_path)) { + /* Path too long; install as a regular file. */ + got_object_blob_rewind(blob); + return install_blob(worktree, ondisk_path, path, + GOT_DEFAULT_FILE_MODE, st_mode, blob, + restoring_missing_file, reverting_versioned_file, + repo, progress_cb, progress_arg); + } + if (len > 0) { + /* Skip blob object header first time around. */ + memcpy(target_path + target_len, buf + hdrlen, + len - hdrlen); + target_len += len - hdrlen; + hdrlen = 0; + } + } while (len != 0); + target_path[target_len] = '\0'; + + /* + * Relative symlink target lookup should begin at the directory + * in which the blob object is being installed. + */ + if (!got_path_is_absolute(target_path)) { + char *parent = dirname(ondisk_path); + if (asprintf(&abspath, "%s/%s", parent, target_path) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + } + + /* + * unveil(2) restricts our view of paths in the filesystem. + * ENOENT will occur if a link target path does not exist or + * if it points outside our unveiled path space. + */ + resolved_path = realpath(abspath ? abspath : target_path, NULL); + if (resolved_path == NULL) { + if (errno != ENOENT) + return got_error_from_errno2("realpath", target_path); + } + + /* Only allow symlinks pointing at paths within the work tree. */ + if (!got_path_is_child(resolved_path ? resolved_path : target_path, + worktree->root_path, strlen(worktree->root_path))) { + /* install as a regular file */ + got_object_blob_rewind(blob); + err = install_blob(worktree, ondisk_path, path, + GOT_DEFAULT_FILE_MODE, st_mode, blob, + restoring_missing_file, reverting_versioned_file, + repo, progress_cb, progress_arg); + goto done; + } + + if (symlink(target_path, ondisk_path) == -1) { + if (errno == ENOENT) { + char *parent = dirname(ondisk_path); + if (parent == NULL) { + err = got_error_from_errno2("dirname", + ondisk_path); + goto done; + } + err = add_dir_on_disk(worktree, parent); + if (err) + goto done; + /* + * Retry, and fall through to error handling + * below if this second attempt fails. + */ + if (symlink(target_path, ondisk_path) != -1) { + err = NULL; /* success */ + goto done; + } + } + + /* Handle errors from first or second creation attempt. */ + if (errno == EEXIST) { + struct stat sb; + ssize_t elen; + char etarget[PATH_MAX]; + if (lstat(ondisk_path, &sb) == -1) { + err = got_error_from_errno2("lstat", + ondisk_path); + goto done; + } + if (!S_ISLNK(sb.st_mode)) { + err = got_error_path(ondisk_path, + GOT_ERR_FILE_OBSTRUCTED); + goto done; + } + elen = readlink(ondisk_path, etarget, sizeof(etarget)); + if (elen == -1) { + err = got_error_from_errno2("readlink", + ondisk_path); + goto done; + } + if (elen == target_len && + memcmp(etarget, target_path, target_len) == 0) + err = NULL; + else + err = got_error_path(ondisk_path, + GOT_ERR_FILE_OBSTRUCTED); + } else if (errno == ENAMETOOLONG) { + /* bad target path; install as a regular file */ + got_object_blob_rewind(blob); + err = install_blob(worktree, ondisk_path, path, + GOT_DEFAULT_FILE_MODE, st_mode, blob, + restoring_missing_file, reverting_versioned_file, + repo, progress_cb, progress_arg); + } else if (errno == ENOTDIR) { + err = got_error_path(ondisk_path, + GOT_ERR_FILE_OBSTRUCTED); + } else { + err = got_error_from_errno3("symlink", + target_path, ondisk_path); + } + } +done: + free(resolved_path); + free(abspath); + return err; +} + +static const struct got_error * +install_blob(struct got_worktree *worktree, const char *ondisk_path, + const char *path, mode_t te_mode, mode_t st_mode, + struct got_blob_object *blob, int restoring_missing_file, + int reverting_versioned_file, struct got_repository *repo, + got_worktree_checkout_cb progress_cb, void *progress_arg) +{ + const struct got_error *err = NULL; int fd = -1; size_t len, hdrlen; int update = 0; char *tmppath = NULL; + + if (S_ISLNK(te_mode)) + return install_symlink(worktree, ondisk_path, path, te_mode, + st_mode, blob, restoring_missing_file, + reverting_versioned_file, repo, progress_cb, progress_arg); fd = open(ondisk_path, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW, GOT_DEFAULT_FILE_MODE); blob - 68a5558798cac114d8be26ae680a0efced072896 blob + 8bb0b0d52dfb1516850a25bf3e9b77204b3ae314 --- regress/cmdline/checkout.sh +++ regress/cmdline/checkout.sh @@ -494,13 +494,80 @@ function test_checkout_into_nonempty_dir { echo 'M alpha' > $testroot/stdout.expected (cd $testroot/wt && got status > $testroot/stdout) + + cmp -s $testroot/stdout.expected $testroot/stdout + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + fi + test_done "$testroot" "$ret" +} + +function test_checkout_symlink { + local testroot=`test_init checkout_symlink` + + (cd $testroot/repo && ln -s alpha alpha.link) + (cd $testroot/repo && ln -s epsilon epsilon.link) + (cd $testroot/repo && ln -s /etc/passwd passwd.link) + (cd $testroot/repo && git add .) + git_commit $testroot/repo -m "add a symlink" + + got checkout $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + if ! [ -h $testroot/wt/alpha.link ]; then + echo "alpha.link is not a symlink" + test_done "$testroot" "1" + return 1 + fi + + readlink $testroot/wt/alpha.link > $testroot/stdout + echo "alpha" > $testroot/stdout.expected + cmp -s $testroot/stdout.expected $testroot/stdout + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + if ! [ -h $testroot/wt/epsilon.link ]; then + echo "epsilon.link is not a symlink" + test_done "$testroot" "1" + return 1 + fi + + readlink $testroot/wt/epsilon.link > $testroot/stdout + echo "epsilon" > $testroot/stdout.expected cmp -s $testroot/stdout.expected $testroot/stdout ret="$?" if [ "$ret" != "0" ]; then diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 fi + + if [ -h $testroot/wt/passwd.link ]; then + echo -n "passwd.link symlink points outside of work tree: " >&2 + readlink $testroot/wt/passwd.link >&2 + test_done "$testroot" "1" + return 1 + fi + + echo -n "/etc/passwd" > $testroot/content.expected + cp $testroot/wt/passwd.link $testroot/content + + cmp -s $testroot/content.expected $testroot/content + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/content.expected $testroot/content + fi test_done "$testroot" "$ret" + } run_test test_checkout_basic @@ -512,3 +579,4 @@ run_test test_checkout_tag run_test test_checkout_ignores_submodules run_test test_checkout_read_only run_test test_checkout_into_nonempty_dir +run_test test_checkout_symlink