"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Stefan Sperling <stsp@stsp.name>
Subject:
install symbolic links in the work tree
To:
gameoftrees@openbsd.org
Date:
Thu, 28 May 2020 12:51:58 +0200

Download raw body.

Thread
This adds support for creating symbolic links in the work tree.

At present symbolic links in the repository are installed as regular
files in the work tree, which contain the link target path.
We keep doing so as a fallback if a particular symbolic link cannot
be installed.

A big concern here is to avoid creating symbolic links which point
anywhere outside of the work tree. Did I get this right?

Only tested with 'got checkout' yet, but more tests will likely follow.

ok?

diff bcbc22724ddb271a0f40cdfec5dcb49f2e52b8da /home/stsp/src/got
blob - 54c7093a4d0bc68cfd3ef1939b7c80f71106e777
file + include/got_object.h
--- 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
file + lib/object.c
--- 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 - 1ad69b7dff719dd615ae921d1fc94b3ec3fabf39
file + lib/worktree.c
--- lib/worktree.c
+++ lib/worktree.c
@@ -943,18 +943,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 - abba81a65d3c964e736faa7f7c27c92fd429bb1f
file + regress/cmdline/checkout.sh
--- regress/cmdline/checkout.sh
+++ 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