Commit Diff


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