/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2003-2007 Tim Kientzle
 * All rights reserved.
 */
#include "test.h"

#if defined(__CYGWIN__)
# include <limits.h>
# include <sys/cygwin.h>
#endif
#if defined(_WIN32) && !defined(__CYGWIN__)
# include <direct.h>
#endif

/*
 * Try to figure out how deep we can go in our tests.  Assumes that
 * the first call to this function has the longest starting cwd (which
 * is currently "<testdir>/original").  This is mostly to work around
 * limits in our Win32 support.
 *
 * Background: On Posix systems, PATH_MAX is merely a limit on the
 * length of the string passed into a system call.  By repeatedly
 * calling chdir(), you can work with arbitrarily long paths on such
 * systems.  In contrast, Win32 APIs apply PATH_MAX limits to the full
 * absolute path, so the permissible length of a system call argument
 * varies with the cwd. Some APIs actually enforce limits
 * significantly less than PATH_MAX to ensure that you can create
 * files within the current working directory.  The Win32 limits also
 * apply to Cygwin before 1.7.
 *
 * Someday, I want to convert the Win32 support to use newer
 * wide-character paths with '\\?\' prefix, which has a 32k PATH_MAX
 * instead of the rather anemic 260 character limit of the older
 * system calls.  Then we can drop this mess (unless we want to
 * continue to special-case Cygwin 1.5 and earlier).
 */
static int
compute_loop_max(void)
{
#if defined(_WIN32) && !defined(__CYGWIN__)
	static int LOOP_MAX = 0;
	char buf[MAX_PATH];
	size_t cwdlen;

	if (LOOP_MAX == 0) {
		assert(_getcwd(buf, MAX_PATH) != NULL);
		cwdlen = strlen(buf);
		/* 12 characters = length of 8.3 filename */
		/* 4 characters = length of "/../" used in symlink tests */
		/* 1 character = length of extra "/" separator */
		LOOP_MAX = MAX_PATH - (int)cwdlen - 12 - 4 - 1;
	}
	return LOOP_MAX;
#elif defined(__CYGWIN__) && !defined(HAVE_CYGWIN_CONV_PATH)
	static int LOOP_MAX = 0;
	if (LOOP_MAX == 0) {
		char wbuf[PATH_MAX];
		char pbuf[PATH_MAX];
		size_t wcwdlen;
		size_t pcwdlen;
	        size_t cwdlen;
		assert(getcwd(pbuf, PATH_MAX) != NULL);
		pcwdlen = strlen(pbuf);
		cygwin_conv_to_full_win32_path(pbuf, wbuf);
		wcwdlen = strlen(wbuf);
		cwdlen = ((wcwdlen > pcwdlen) ? wcwdlen : pcwdlen);
		/* Cygwin helper needs an extra few characters. */
		LOOP_MAX = PATH_MAX - (int)cwdlen - 12 - 4 - 4;
	}
	return LOOP_MAX;
#else
	/* cygwin-1.7 ends up here, along with "normal" unix */
	return 200; /* restore pre-r278 depth */
#endif
}

/* filenames[i] is a distinctive filename of length i. */
/* To simplify interpreting failures, each filename ends with a
 * decimal integer which is the length of the filename.  E.g., A
 * filename ending in "_92" is 92 characters long.  To detect errors
 * which drop or misplace characters, the filenames use a repeating
 * "abcdefghijklmnopqrstuvwxyz..." pattern. */
static char *filenames[201];

static void
compute_filenames(void)
{
	char buff[250];
	size_t i,j;

	filenames[0] = strdup("");
	filenames[1] = strdup("1");
	filenames[2] = strdup("a2");
	for (i = 3; i < sizeof(filenames)/sizeof(filenames[0]); ++i) {
		/* Fill with "abcdefghij..." */
		for (j = 0; j < i; ++j)
			buff[j] = 'a' + (j % 26);
		buff[j--] = '\0';
		/* Work from the end to fill in the number portion. */
		buff[j--] = '0' + (i % 10);
		if (i > 9) {
			buff[j--] = '0' + ((i / 10) % 10);
			if (i > 99)
				buff[j--] = '0' + (char)(i / 100);
		}
		buff[j] = '_';
		/* Guard against obvious screwups in the above code. */
		assertEqualInt(strlen(buff), i);
		filenames[i] = strdup(buff);
	}
}

static void
create_tree(void)
{
	char buff[260];
	char buff2[260];
	int i;
	int LOOP_MAX;

	compute_filenames();

	/* Log that we'll be omitting some checks. */
	if (!canSymlink()) {
		skipping("Symlink checks");
	}

	assertMakeDir("original", 0775);
	assertEqualInt(0, chdir("original"));
	LOOP_MAX = compute_loop_max();

	assertMakeDir("f", 0775);
	assertMakeDir("l", 0775);
	assertMakeDir("m", 0775);
	assertMakeDir("s", 0775);
	assertMakeDir("d", 0775);

	for (i = 1; i < LOOP_MAX; i++) {
		failure("Internal sanity check failed: i = %d", i);
		assert(filenames[i] != NULL);

		snprintf(buff, sizeof(buff), "f/%s", filenames[i]);
		assertMakeFile(buff, 0777, buff);

		/* Create a link named "l/abcdef..." to the above. */
		snprintf(buff2, sizeof(buff2), "l/%s", filenames[i]);
		assertMakeHardlink(buff2, buff);

		/* Create a link named "m/abcdef..." to the above. */
		snprintf(buff2, sizeof(buff2), "m/%s", filenames[i]);
		assertMakeHardlink(buff2, buff);

		if (canSymlink()) {
			/* Create a symlink named "s/abcdef..." to the above. */
			snprintf(buff, sizeof(buff), "s/%s", filenames[i]);
			snprintf(buff2, sizeof(buff2), "../f/%s", filenames[i]);
			failure("buff=\"%s\" buff2=\"%s\"", buff, buff2);
			assertMakeSymlink(buff, buff2, 0);
		}
		/* Create a dir named "d/abcdef...". */
		buff[0] = 'd';
		failure("buff=\"%s\"", buff);
		assertMakeDir(buff, 0775);
	}

	assertEqualInt(0, chdir(".."));
}

#define LIMIT_NONE 200
#define LIMIT_USTAR 100

static void
verify_tree(size_t limit, const char *format)
{
	char name1[260];
	char name2[260];
	size_t i, LOOP_MAX;

	LOOP_MAX = compute_loop_max();

	/* Generate the names we know should be there and verify them. */
	for (i = 1; i < LOOP_MAX; i++) {
		/* Verify a file named "f/abcdef..." */
		snprintf(name1, sizeof(name1), "f/%s", filenames[i]);
		if (i <= limit) {
			failure("Verifying %s", format);
			assertFileExists(name1);
			assertFileContents(name1, (int)strlen(name1), name1);
		}

		snprintf(name2, sizeof(name2), "l/%s", filenames[i]);
		if (i + 2 <= limit) {
			/* Verify hardlink "l/abcdef..." */
			failure("Verifying %s", format);
			assertIsHardlink(name1, name2);
			/* Verify hardlink "m/abcdef..." */
			name2[0] = 'm';
			assertIsHardlink(name1, name2);
		}

		if (canSymlink()) {
			/* Verify symlink "s/abcdef..." */
			snprintf(name1, sizeof(name1), "s/%s", filenames[i]);
			snprintf(name2, sizeof(name2), "../f/%s", filenames[i]);
			if (strlen(name2) <= limit) {
				failure("Verifying %s", format);
				assertIsSymlink(name1, name2, 0);
			}
		}

		/* Verify dir "d/abcdef...". */
		snprintf(name1, sizeof(name1), "d/%s", filenames[i]);
		if (i + 1 <= limit) { /* +1 for trailing slash */
			failure("Verifying %s", format);
			if (assertIsDir(name1, -1)) {
				/* TODO: opendir/readdir this
				 * directory and make sure
				 * it's empty.
				 */
			}
		}
	}

#if !defined(_WIN32) || defined(__CYGWIN__)
	{
		const char *dp;
		/* Now make sure nothing is there that shouldn't be. */
		for (dp = "dflms"; *dp != '\0'; ++dp) {
			DIR *d;
			struct dirent *de;
			char dir[2];
			dir[0] = *dp; dir[1] = '\0';
			d = opendir(dir);
			failure("Unable to open dir '%s' for testing %s", dir, format);
			if (!assert(d != NULL))
				continue;
			while ((de = readdir(d)) != NULL) {
				char *p = de->d_name;
				if (p[0] == '.')
					continue;
				switch(dp[0]) {
				case 'l': case 'm': case 'd':
					failure("strlen(p)=%zu", strlen(p));
					assert(strlen(p) < limit);
					assertEqualString(p,
					    filenames[strlen(p)]);
					break;
				case 'f': case 's':
					failure("strlen(p)=%zu", strlen(p));
					assert(strlen(p) < limit + 1);
					assertEqualString(p,
					    filenames[strlen(p)]);
					break;
				default:
					failure("File %s shouldn't be here", p);
					assert(0);
				}
			}
			closedir(d);
		}
	}
#endif
}

static void
copy_basic(const char *extra_args, const char *name)
{
	int r;

	/* NOTE: for proper operation on cygwin-1.5 and windows, the
	 * length of the name of the directory below must be
	 * less than or equal to the length of the name of the original
	 * directory, "original"  This restriction derives from the
	 * extremely limited pathname lengths on those platforms.
	 */
	assertMakeDir(name, 0775);
	assertEqualInt(0, chdir(name));

	/*
	 * Use the tar program to create an archive.
	 */
	r = systemf("%s cf archive %s -C ../original f d l m s >pack.out 2>pack.err",
	    testprog, extra_args);
	failure("Error invoking \"%s cf archive %s\"", testprog, extra_args);
	assertEqualInt(r, 0);

	/* Verify that nothing went to stdout or stderr. */
	assertEmptyFile("pack.err");
	assertEmptyFile("pack.out");

	/*
	 * Use tar to unpack the archive into another directory.
	 */
	r = systemf("%s xf archive >unpack.out 2>unpack.err", testprog);
	failure("Error invoking %s xf archive", testprog);
	assertEqualInt(r, 0);

	/* Verify that nothing went to stdout or stderr. */
	assertEmptyFile("unpack.err");
	assertEmptyFile("unpack.out");

	verify_tree(LIMIT_NONE, name);
	assertEqualInt(0, chdir(".."));
}

static void
copy_ustar(void)
{
	const char *target = "ustar";
	int r;

	/* NOTE: for proper operation on cygwin-1.5 and windows, the
	 * length of the name of the directory below, "ustar", must be
	 * less than or equal to the length of the name of the original
	 * directory, "original"  This restriction derives from the
	 * extremely limited pathname lengths on those platforms.
	 */
	assertMakeDir(target, 0775);
	assertEqualInt(0, chdir(target));

	/*
	 * Use the tar program to create an archive.
	 */
	r = systemf("%s cf archive --format=ustar -C ../original f d l m s >pack.out 2>pack.err",
	    testprog);
	failure("Error invoking \"%s cf archive --format=ustar\"", testprog);
	assertEqualInt(r, 0);

	/* Verify that nothing went to stdout. */
	assertEmptyFile("pack.out");
	/* Stderr is non-empty, since there are a bunch of files
	 * with filenames too long to archive. */

	/*
	 * Use tar to unpack the archive into another directory.
	 */
	r = systemf("%s xf archive >unpack.out 2>unpack.err", testprog);
	failure("Error invoking %s xf archive", testprog);
	assertEqualInt(r, 0);

	/* Verify that nothing went to stdout or stderr. */
	assertEmptyFile("unpack.err");
	assertEmptyFile("unpack.out");

	verify_tree(LIMIT_USTAR, "ustar");
	assertEqualInt(0, chdir(".."));
}

DEFINE_TEST(test_copy)
{
	assertUmask(0);
	create_tree(); /* Create sample files in "original" dir. */

	/* Test simple "tar -c | tar -x" pipeline copy. */
	copy_basic("", "default");

	/* Same, but constrain to ustar format. */
	copy_ustar();

	/* Same, but with pax format. */
	copy_basic(" --format pax", "pax");
}
