Commit d4b953f80005bc08c790ffe3ef0776ced370706c

Edward Thomson 2020-06-02T09:26:11

Merge pull request #5528 from libgit2/ethomson/clar_internal clar: use internal functions instead of /bin/cp and /bin/rm

diff --git a/tests/clar/fs.h b/tests/clar/fs.h
index 7c7dde6..87d3451 100644
--- a/tests/clar/fs.h
+++ b/tests/clar/fs.h
@@ -1,3 +1,11 @@
+/*
+ * By default, use a read/write loop to copy files on POSIX systems.
+ * On Linux, use sendfile by default as it's slightly faster.  On
+ * macOS, we avoid fcopyfile by default because it's slightly slower.
+ */
+#undef USE_FCOPYFILE
+#define USE_SENDFILE 1
+
 #ifdef _WIN32
 
 #define RM_RETRY_COUNT	5
@@ -254,74 +262,213 @@ cl_fs_cleanup(void)
 
 #include <errno.h>
 #include <string.h>
+#include <limits.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#if defined(__linux__)
+# include <sys/sendfile.h>
+#endif
 
-static int
-shell_out(char * const argv[])
+#if defined(__APPLE__) || defined(__FreeBSD__)
+# include <copyfile.h>
+#endif
+
+static void basename_r(const char **out, int *out_len, const char *in)
 {
-	int status, piderr;
-	pid_t pid;
+	size_t in_len = strlen(in), start_pos;
+
+	for (in_len = strlen(in); in_len; in_len--) {
+		if (in[in_len - 1] != '/')
+			break;
+	}
+
+	for (start_pos = in_len; start_pos; start_pos--) {
+		if (in[start_pos - 1] == '/')
+			break;
+	}
 
-	pid = fork();
+	cl_assert(in_len - start_pos < INT_MAX);
 
-	if (pid < 0) {
-		fprintf(stderr,
-			"System error: `fork()` call failed (%d) - %s\n",
-			errno, strerror(errno));
-		exit(-1);
+	if (in_len - start_pos > 0) {
+		*out = &in[start_pos];
+		*out_len = (in_len - start_pos);
+	} else {
+		*out = "/";
+		*out_len = 1;
 	}
+}
+
+static char *joinpath(const char *dir, const char *base, int base_len)
+{
+	char *out;
+	int len;
 
-	if (pid == 0) {
-		execv(argv[0], argv);
+	if (base_len == -1) {
+		size_t bl = strlen(base);
+
+		cl_assert(bl < INT_MAX);
+		base_len = (int)bl;
 	}
 
-	do {
-		piderr = waitpid(pid, &status, WUNTRACED);
-	} while (piderr < 0 && (errno == EAGAIN || errno == EINTR));
+	len = strlen(dir) + base_len + 2;
+	cl_assert(len > 0);
+
+	cl_assert(out = malloc(len));
+	cl_assert(snprintf(out, len, "%s/%.*s", dir, base_len, base) < len);
+
+	return out;
+}
+
+static void
+fs_copydir_helper(const char *source, const char *dest, int dest_mode)
+{
+	DIR *source_dir;
+	struct dirent *d;
+
+	mkdir(dest, dest_mode);
+
+	cl_assert_(source_dir = opendir(source), "Could not open source dir");
+	while ((d = (errno = 0, readdir(source_dir))) != NULL) {
+		char *child;
+
+		if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+			continue;
+
+		child = joinpath(source, d->d_name, -1);
+		fs_copy(child, dest);
+		free(child);
+	}
+
+	cl_assert_(errno == 0, "Failed to iterate source dir");
+
+	closedir(source_dir);
+}
+
+static void
+fs_copyfile_helper(const char *source, size_t source_len, const char *dest, int dest_mode)
+{
+	int in, out;
+
+	cl_must_pass((in = open(source, O_RDONLY)));
+	cl_must_pass((out = open(dest, O_WRONLY|O_CREAT|O_TRUNC, dest_mode)));
+
+#if USE_FCOPYFILE && (defined(__APPLE__) || defined(__FreeBSD__))
+	((void)(source_len)); /* unused */
+	cl_must_pass(fcopyfile(in, out, 0, COPYFILE_DATA));
+#elif USE_SENDFILE && defined(__linux__)
+	{
+		ssize_t ret = 0;
+
+		while (source_len && (ret = sendfile(out, in, NULL, source_len)) > 0) {
+			source_len -= (size_t)ret;
+		}
+		cl_assert(ret >= 0);
+	}
+#else
+	{
+		char buf[131072];
+		ssize_t ret;
+
+		((void)(source_len)); /* unused */
 
-	return WEXITSTATUS(status);
+		while ((ret = read(in, buf, sizeof(buf))) > 0) {
+			size_t len = (size_t)ret;
+
+			while (len && (ret = write(out, buf, len)) > 0) {
+				cl_assert(ret <= (ssize_t)len);
+				len -= ret;
+			}
+			cl_assert(ret >= 0);
+		}
+		cl_assert(ret == 0);
+	}
+#endif
+
+	close(in);
+	close(out);
 }
 
 static void
-fs_copy(const char *_source, const char *dest)
+fs_copy(const char *source, const char *_dest)
 {
-	char *argv[5];
-	char *source;
-	size_t source_len;
+	char *dbuf = NULL;
+	const char *dest;
+	struct stat source_st, dest_st;
+
+	cl_must_pass_(lstat(source, &source_st), "Failed to stat copy source");
 
-	source = strdup(_source);
-	source_len = strlen(source);
+	if (lstat(_dest, &dest_st) == 0) {
+		const char *base;
+		int base_len;
 
-	if (source[source_len - 1] == '/')
-		source[source_len - 1] = 0;
+		/* Target exists and is directory; append basename */
+		cl_assert(S_ISDIR(dest_st.st_mode));
 
-	argv[0] = "/bin/cp";
-	argv[1] = "-R";
-	argv[2] = source;
-	argv[3] = (char *)dest;
-	argv[4] = NULL;
+		basename_r(&base, &base_len, source);
+		cl_assert(base_len < INT_MAX);
 
-	cl_must_pass_(
-		shell_out(argv),
-		"Failed to copy test fixtures to sandbox"
-	);
+		dbuf = joinpath(_dest, base, base_len);
+		dest = dbuf;
+	} else if (errno != ENOENT) {
+		cl_fail("Cannot copy; cannot stat destination");
+	} else {
+		dest = _dest;
+	}
 
-	free(source);
+	if (S_ISDIR(source_st.st_mode)) {
+		fs_copydir_helper(source, dest, source_st.st_mode);
+	} else {
+		fs_copyfile_helper(source, source_st.st_size, dest, source_st.st_mode);
+	}
+
+	free(dbuf);
 }
 
 static void
-fs_rm(const char *source)
+fs_rmdir_helper(const char *path)
 {
-	char *argv[4];
+	DIR *dir;
+	struct dirent *d;
+
+	cl_assert_(dir = opendir(path), "Could not open dir");
+	while ((d = (errno = 0, readdir(dir))) != NULL) {
+		char *child;
+
+		if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+			continue;
 
-	argv[0] = "/bin/rm";
-	argv[1] = "-Rf";
-	argv[2] = (char *)source;
-	argv[3] = NULL;
+		child = joinpath(path, d->d_name, -1);
+		fs_rm(child);
+		free(child);
+	}
+
+	cl_assert_(errno == 0, "Failed to iterate source dir");
+	closedir(dir);
+
+	cl_must_pass_(rmdir(path), "Could not remove directory");
+}
 
-	cl_must_pass_(
-		shell_out(argv),
-		"Failed to cleanup the sandbox"
-	);
+static void
+fs_rm(const char *path)
+{
+	struct stat st;
+
+	if (lstat(path, &st)) {
+		if (errno == ENOENT)
+			return;
+
+		cl_fail("Cannot copy; cannot stat destination");
+	}
+
+	if (S_ISDIR(st.st_mode)) {
+		fs_rmdir_helper(path);
+	} else {
+		cl_must_pass(unlink(path));
+	}
 }
 
 void