Commit 8ba819a3547825c0e0d657a7e41610da16f6cd4f

Stefan Sperling 2020-07-23T14:21:27

let 'got checkout' create symlinks in a work tree

diff --git a/include/got_object.h b/include/got_object.h
index 54c7093..b0dceed 100644
--- a/include/got_object.h
+++ b/include/got_object.h
@@ -254,6 +254,9 @@ const uint8_t *got_object_blob_get_read_buf(struct got_blob_object *);
 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
diff --git a/lib/object.c b/lib/object.c
index 9a3beff..0138ca9 100644
--- a/lib/object.c
+++ b/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)
 {
diff --git a/lib/worktree.c b/lib/worktree.c
index 985b0d7..1f4dfd3 100644
--- a/lib/worktree.c
+++ b/lib/worktree.c
@@ -935,6 +935,158 @@ 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,
@@ -948,6 +1100,11 @@ install_blob(struct got_worktree *worktree, const char *ondisk_path,
 	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);
 	if (fd == -1) {
diff --git a/regress/cmdline/checkout.sh b/regress/cmdline/checkout.sh
index 68a5558..8bb0b0d 100755
--- a/regress/cmdline/checkout.sh
+++ b/regress/cmdline/checkout.sh
@@ -503,6 +503,73 @@ function test_checkout_into_nonempty_dir {
 	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
 run_test test_checkout_dir_exists
 run_test test_checkout_dir_not_empty
@@ -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