/*
   Unix SMB/CIFS implementation.
   SMB torture tester
   Copyright (C) Guenther Deschner 2010

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "source3/include/includes.h"
#include "system/dir.h"
#include "torture/smbtorture.h"
#include "auth/credentials/credentials.h"
#include "lib/cmdline/cmdline.h"
#include "source3/include/libsmb_internal.h"
#include <libsmbclient.h>
#include "torture/libsmbclient/proto.h"
#include "lib/param/loadparm.h"
#include "lib/param/param_global.h"
#include "libcli/smb/smb_constants.h"
#include "dynconfig.h"
#include "lib/util/time.h"

/* test string to compare with when debug_callback is called */
#define TEST_STRING "smbc_setLogCallback test"

/* Dummy log callback function */
static void debug_callback(void *private_ptr, int level, const char *msg)
{
	bool *found = private_ptr;
	if (strstr(msg, TEST_STRING) != NULL) {
		*found = true;
	}
	return;
}

static void auth_callback(const char *srv,
			  const char *shr,
			  char *wg, int wglen,
			  char *un, int unlen,
			  char *pw, int pwlen)
{
	const char *workgroup =
		cli_credentials_get_domain(samba_cmdline_get_creds());
	const char *username =
		cli_credentials_get_username(samba_cmdline_get_creds());
	const char *password =
		cli_credentials_get_password(samba_cmdline_get_creds());
	ssize_t ret;

	if (workgroup != NULL) {
		ret = strlcpy(wg, workgroup, wglen);
		if (ret >= wglen) {
			abort();
		}
	}

	if (username != NULL) {
		ret = strlcpy(un, username, unlen);
		if (ret >= unlen) {
			abort();
		}
	}

	if (password != NULL) {
		ret = strlcpy(pw, password, pwlen);
		if (ret >= pwlen) {
			abort();
		}
	}
};

bool torture_libsmbclient_init_context(struct torture_context *tctx,
				       SMBCCTX **ctx_p)
{
	const char *workgroup =
		cli_credentials_get_domain(samba_cmdline_get_creds());
	const char *username =
		cli_credentials_get_username(samba_cmdline_get_creds());
	const char *client_proto =
		torture_setting_string(tctx, "clientprotocol", NULL);
	SMBCCTX *ctx = NULL;
	SMBCCTX *p = NULL;
	bool ok = true;
	int dbglevel = DEBUGLEVEL;

	ctx = smbc_new_context();
	torture_assert_not_null_goto(tctx,
				     ctx,
				     ok,
				     out,
				     "Failed to create new context");

	p = smbc_init_context(ctx);
	torture_assert_not_null_goto(tctx,
				     p,
				     ok,
				     out,
				     "Failed to initialize context");

	smbc_setDebug(ctx, dbglevel);
	smbc_setOptionDebugToStderr(ctx, 1);

	if (workgroup != NULL) {
		smbc_setWorkgroup(ctx, workgroup);
	}
	if (username != NULL) {
		smbc_setUser(ctx, username);
	}

	smbc_setFunctionAuthData(ctx, auth_callback);

	if (client_proto != NULL) {
		smbc_setOptionProtocols(ctx, client_proto, client_proto);
	}

	*ctx_p = ctx;

out:
	if (!ok) {
		smbc_free_context(ctx, 1);
	}

	return ok;
}

static bool torture_libsmbclient_version(struct torture_context *tctx)
{
	torture_comment(tctx, "Testing smbc_version\n");

	torture_assert(tctx, smbc_version(), "failed to get version");

	return true;
}

static bool torture_libsmbclient_initialize(struct torture_context *tctx)
{
	SMBCCTX *ctx;
	bool ret = false;

	torture_comment(tctx, "Testing smbc_new_context\n");

	ctx = smbc_new_context();
	torture_assert(tctx, ctx, "failed to get new context");

	torture_comment(tctx, "Testing smbc_init_context\n");

	torture_assert(tctx, smbc_init_context(ctx), "failed to init context");

	smbc_setLogCallback(ctx, &ret, debug_callback);
	DEBUG(0, (TEST_STRING"\n"));
	torture_assert(tctx, ret, "Failed debug_callback not called");
	ret = false;
	smbc_setLogCallback(ctx, NULL, NULL);
	DEBUG(0, (TEST_STRING"\n"));
	torture_assert(tctx, !ret, "Failed debug_callback called");

	smbc_free_context(ctx, 1);

	return true;
}

static bool torture_libsmbclient_setConfiguration(struct torture_context *tctx)
{
	SMBCCTX *ctx;
	struct loadparm_global *global_config = NULL;
	const char *new_smb_conf = torture_setting_string(tctx,
				"replace_smbconf",
				"");

	ctx = smbc_new_context();
	torture_assert_not_null(tctx, ctx, "failed to get new context");

	torture_assert_not_null(
		tctx, smbc_init_context(ctx), "failed to init context");

	torture_comment(tctx, "Testing smbc_setConfiguration - new file %s\n",
		new_smb_conf);

	global_config = get_globals();
	torture_assert(tctx, global_config, "Global Config is NULL");

	/* check configuration before smbc_setConfiguration call */
	torture_comment(tctx, "'workgroup' before setConfiguration %s\n",
			global_config->workgroup);
	torture_comment(tctx, "'client min protocol' before "
			"setConfiguration %d\n",
			global_config->client_min_protocol);
	torture_comment(tctx, "'client max protocol' before "
			"setConfiguration %d\n",
			global_config->_client_max_protocol);
	torture_comment(tctx, "'client signing' before setConfiguration %d\n",
			global_config->client_signing);
	torture_comment(tctx, "'deadtime' before setConfiguration %d\n",
			global_config->deadtime);

	torture_assert_int_equal(tctx, smbc_setConfiguration(ctx, new_smb_conf),
			0, "setConfiguration conf file not found");

	/* verify configuration */
	torture_assert_str_equal(tctx, global_config->workgroup,
			"NEW_WORKGROUP",
			"smbc_setConfiguration failed, "
			"'workgroup' not updated");
	torture_assert_int_equal(tctx, global_config->client_min_protocol, PROTOCOL_NT1,
			"smbc_setConfiguration failed, 'client min protocol' "
			"not updated");
	torture_assert_int_equal(tctx, global_config->_client_max_protocol, PROTOCOL_SMB3_00,
			"smbc_setConfiguration failed, 'client max protocol' "
			"not updated");
	torture_assert_int_equal(tctx, global_config->client_signing, 1,
			"smbc_setConfiguration failed, 'client signing' "
			"not updated");
	torture_assert_int_equal(tctx, global_config->deadtime, 5,
			"smbc_setConfiguration failed, 'deadtime' not updated");

	/* Restore configuration to default */
	smbc_setConfiguration(ctx, get_dyn_CONFIGFILE());

	smbc_free_context(ctx, 1);

	return true;
}

static bool test_opendir(struct torture_context *tctx,
			 SMBCCTX *ctx,
			 const char *fname,
			 bool expect_success)
{
	int handle, ret;

	torture_comment(tctx, "Testing smbc_opendir(%s)\n", fname);

	handle = smbc_opendir(fname);
	if (!expect_success) {
		return true;
	}
	if (handle < 0) {
		torture_fail(tctx, talloc_asprintf(tctx, "failed to obain file handle for '%s'", fname));
	}

	ret = smbc_closedir(handle);
	torture_assert_int_equal(tctx, ret, 0,
		talloc_asprintf(tctx, "failed to close file handle for '%s'", fname));

	return true;
}

static bool torture_libsmbclient_opendir(struct torture_context *tctx)
{
	size_t i;
	SMBCCTX *ctx;
	bool ret = true;
	const char *bad_urls[] = {
		"",
		NULL,
		"smb",
		"smb:",
		"smb:/",
		"smb:///",
		"bms://",
		":",
		":/",
		"://",
		":///",
		"/",
		"//",
		"///"
	};
	const char *good_urls[] = {
		"smb://",
		"smb://WORKGROUP",
		"smb://WORKGROUP/"
	};

	torture_assert(tctx, torture_libsmbclient_init_context(tctx, &ctx), "");
	smbc_set_context(ctx);

	for (i=0; i < ARRAY_SIZE(bad_urls); i++) {
		ret &= test_opendir(tctx, ctx, bad_urls[i], false);
	}
	for (i=0; i < ARRAY_SIZE(good_urls); i++) {
		ret &= test_opendir(tctx, ctx, good_urls[i], true);
	}

	smbc_free_context(ctx, 1);

	return ret;
}

static bool torture_libsmbclient_readdirplus(struct torture_context *tctx)
{
	SMBCCTX *ctx;
	int ret = -1;
	int dhandle = -1;
	int fhandle = -1;
	bool found = false;
	const char *filename = NULL;
	const char *smburl = torture_setting_string(tctx, "smburl", NULL);

	if (smburl == NULL) {
		torture_fail(tctx,
			"option --option=torture:smburl="
			"smb://user:password@server/share missing\n");
	}

	torture_assert(tctx, torture_libsmbclient_init_context(tctx, &ctx), "");
	smbc_set_context(ctx);

	filename = talloc_asprintf(tctx,
				"%s/test_readdirplus.txt",
				smburl);
	if (filename == NULL) {
		torture_fail(tctx,
			"talloc fail\n");
	}
	/* Ensure the file doesn't exist. */
	smbc_unlink(filename);

	/* Create it. */
	fhandle = smbc_creat(filename, 0666);
	if (fhandle < 0) {
		torture_fail(tctx,
			talloc_asprintf(tctx,
				"failed to create file '%s': %s",
				filename,
				strerror(errno)));
	}
	ret = smbc_close(fhandle);
	torture_assert_int_equal(tctx,
		ret,
		0,
		talloc_asprintf(tctx,
			"failed to close handle for '%s'",
			filename));

	dhandle = smbc_opendir(smburl);
	if (dhandle < 0) {
		int saved_errno = errno;
		smbc_unlink(filename);
		torture_fail(tctx,
			talloc_asprintf(tctx,
				"failed to obtain "
				"directory handle for '%s' : %s",
				smburl,
				strerror(saved_errno)));
	}

	/* Readdirplus to ensure we see the new file. */
	for (;;) {
		const struct libsmb_file_info *exstat =
			smbc_readdirplus(dhandle);
		if (exstat == NULL) {
			break;
		}
		if (strcmp(exstat->name, "test_readdirplus.txt") == 0) {
			found = true;
			break;
		}
	}

	/* Remove it again. */
	smbc_unlink(filename);
	ret = smbc_closedir(dhandle);
	torture_assert_int_equal(tctx,
		ret,
		0,
		talloc_asprintf(tctx,
			"failed to close directory handle for '%s'",
			smburl));

	smbc_free_context(ctx, 1);

	if (!found) {
		torture_fail(tctx,
			talloc_asprintf(tctx,
				"failed to find file '%s'",
				filename));
	}

	return true;
}

static bool torture_libsmbclient_readdirplus_seek(struct torture_context *tctx)
{
	SMBCCTX *ctx;
	int ret = -1;
	int dhandle = -1;
	int fhandle = -1;
	const char *dname = NULL;
	const char *full_filename[100] = {0};
	const char *filename[100] = {0};
	const struct libsmb_file_info *direntries[102] = {0};
	unsigned int i = 0;
	const char *smburl = torture_setting_string(tctx, "smburl", NULL);
	bool success = false;
	off_t telldir_50 = (off_t)-1;
	off_t telldir_20 = (off_t)-1;
	size_t getdentries_size = 0;
	struct smbc_dirent *getdentries = NULL;
	struct smbc_dirent *dirent_20 = NULL;
	const struct libsmb_file_info *direntries_20 = NULL;
	const struct libsmb_file_info *direntriesplus_20 = NULL;
	const char *plus2_stat_path = NULL;
	struct stat st = {0};
	struct stat st2 = {0};

	torture_assert_not_null(
		tctx,
		smburl,
		"option --option=torture:smburl="
		"smb://user:password@server/share missing\n");

	DEBUG(0,("torture_libsmbclient_readdirplus_seek start\n"));

	torture_assert(tctx, torture_libsmbclient_init_context(tctx, &ctx), "");
	smbc_set_context(ctx);

	dname = talloc_asprintf(tctx,
				"%s/rd_seek",
				smburl);
	torture_assert_not_null_goto(
		tctx, dname, success, done, "talloc fail\n");

	/* Ensure the files don't exist. */
	for (i = 0; i < 100; i++) {
		filename[i] = talloc_asprintf(tctx,
				"test_readdirplus_%u.txt",
				i);
		torture_assert_not_null_goto(
			tctx, filename[i], success, done, "talloc fail");
		full_filename[i] = talloc_asprintf(tctx,
				"%s/%s",
				dname,
				filename[i]);
		torture_assert_not_null_goto(
			tctx, full_filename[i], success, done, "talloc fail");
		(void)smbc_unlink(full_filename[i]);
	}
	/* Ensure the directory doesn't exist. */
	(void)smbc_rmdir(dname);

	/* Create containing directory. */
	ret = smbc_mkdir(dname, 0777);
	torture_assert_goto(
		tctx,
		ret == 0,
		success,
		done,
		talloc_asprintf(tctx,
				"failed to create directory '%s': %s",
				dname,
				strerror(errno)));

	DEBUG(0,("torture_libsmbclient_readdirplus_seek create\n"));

	/* Create them. */
	for (i = 0; i < 100; i++) {
		fhandle = smbc_creat(full_filename[i], 0666);
		if (fhandle < 0) {
			torture_fail_goto(tctx,
				done,
				talloc_asprintf(tctx,
					"failed to create file '%s': %s",
					full_filename[i],
					strerror(errno)));
		}
		ret = smbc_close(fhandle);
		torture_assert_int_equal_goto(tctx,
			ret,
			0,
			success,
			done,
			talloc_asprintf(tctx,
				"failed to close handle for '%s'",
				full_filename[i]));
	}

	DEBUG(0,("torture_libsmbclient_readdirplus_seek enum\n"));

	/* Now enumerate the directory. */
	dhandle = smbc_opendir(dname);
	torture_assert_goto(
		tctx,
		dhandle >= 0,
		success,
		done,
		talloc_asprintf(tctx,
				"failed to obtain "
				"directory handle for '%s' : %s",
				dname,
				strerror(errno)));

	/* Read all the files. 100 we created plus . and .. */
	for (i = 0; i < 102; i++) {
		bool found = false;
		unsigned int j;

		direntries[i] = smbc_readdirplus(dhandle);
		if (direntries[i] == NULL) {
			break;
		}

		/* Store at offset 50. */
		if (i == 50) {
			telldir_50 = smbc_telldir(dhandle);
			torture_assert_goto(
				tctx,
				telldir_50 != (off_t)-1,
				success,
				done,
				talloc_asprintf(tctx,
						"telldir failed file %s\n",
						direntries[i]->name));
		}

		if (ISDOT(direntries[i]->name)) {
			continue;
		}
		if (ISDOTDOT(direntries[i]->name)) {
			continue;
		}

		/* Ensure all our files exist. */
		for (j = 0; j < 100; j++) {
			if (strcmp(direntries[i]->name,
				filename[j]) == 0) {
				found = true;
			}
		}
		torture_assert_goto(
			tctx,
			found,
			success,
			done,
			talloc_asprintf(tctx,
					"failed to find file %s\n",
					direntries[i]->name));
	}

	/*
	 * We're seeking on in-memory lists here, so
	 * whilst the handle is open we really should
	 * get the same files back in the same order.
	 */

	ret = smbc_lseekdir(dhandle, telldir_50);
	torture_assert_int_equal_goto(tctx,
		ret,
		0,
		success,
		done,
		talloc_asprintf(tctx,
			"failed to seek (50) directory handle for '%s'",
			dname));

	DEBUG(0,("torture_libsmbclient_readdirplus_seek seek\n"));

	for (i = 51; i < 102; i++) {
		const struct libsmb_file_info *entry =
				smbc_readdirplus(dhandle);
		torture_assert_goto(
			tctx,
			entry == direntries[i],
			success,
			done,
			talloc_asprintf(tctx,
					"after seek - failed to find "
					"file %s - got %s\n",
					direntries[i]->name,
					entry->name));
	}

	/* Seek back to the start. */
	ret = smbc_lseekdir(dhandle, 0);
	torture_assert_int_equal_goto(tctx,
		ret,
		0,
		success,
		done,
		talloc_asprintf(tctx,
			"failed to seek directory handle to start for '%s'",
			dname));

	/*
	 * Mix getdents/readdir/readdirplus with lseek to ensure
	 * we get the same result.
	 */

	/* Allocate the space for 20 entries.
	 * Tricky as we need to allocate 20 struct smbc_dirent's + space
	 * for the name lengths.
	 */
	getdentries_size = 20 * (sizeof(struct smbc_dirent) +
				strlen("test_readdirplus_1000.txt") + 1);

	getdentries = (struct smbc_dirent *)talloc_array_size(tctx,
						getdentries_size,
						1);
	torture_assert_not_null_goto(
		tctx,
		getdentries,
		success,
		done,
		"talloc fail");

	ret = smbc_getdents(dhandle, getdentries, getdentries_size);
	torture_assert_goto(tctx,
		(ret != -1),
		success,
		done,
		talloc_asprintf(tctx,
			"smbd_getdents(1) for '%s' failed\n",
			dname));

	telldir_20 = smbc_telldir(dhandle);
	torture_assert_goto(
		tctx,
		telldir_20 != (off_t)-1,
		success,
		done,
		"telldir (20) failed\n");

	/* Read another 20. */
	ret = smbc_getdents(dhandle, getdentries, getdentries_size);
	torture_assert_goto(tctx,
		(ret != -1),
		success,
		done,
		talloc_asprintf(tctx,
			"smbd_getdents(2) for '%s' failed\n",
			dname));

	/* Seek back to 20. */
	ret = smbc_lseekdir(dhandle, telldir_20);
	torture_assert_int_equal_goto(tctx,
		ret,
		0,
		success,
		done,
		talloc_asprintf(tctx,
			"failed to seek (20) directory handle for '%s'",
			dname));

	/* Read with readdir. */
	dirent_20 = smbc_readdir(dhandle);
	torture_assert_not_null_goto(
		tctx,
		dirent_20,
		success,
		done,
		"smbc_readdir (20) failed\n");

	/* Ensure the getdents and readdir names are the same. */
	ret = strcmp(dirent_20->name, getdentries[0].name);
	torture_assert_goto(
		tctx,
		ret == 0,
		success,
		done,
		talloc_asprintf(tctx,
				"after seek (20) readdir name mismatch "
				"file %s - got %s\n",
				dirent_20->name,
				getdentries[0].name));

	/* Seek back to 20. */
	ret = smbc_lseekdir(dhandle, telldir_20);
	torture_assert_int_equal_goto(tctx,
		ret,
		0,
		success,
		done,
		talloc_asprintf(tctx,
			"failed to seek (20) directory handle for '%s'",
			dname));
	/* Read with readdirplus. */
	direntries_20 = smbc_readdirplus(dhandle);
	torture_assert_not_null_goto(
		tctx,
		direntries_20,
		success,
		done,
		"smbc_readdirplus (20) failed\n");

	/* Ensure the readdirplus and readdir names are the same. */
	ret = strcmp(dirent_20->name, direntries_20->name);
	torture_assert_goto(
		tctx,
		ret == 0,
		success,
		done,
		talloc_asprintf(tctx,
				"after seek (20) readdirplus name mismatch "
				"file %s - got %s\n",
				dirent_20->name,
				direntries_20->name));

	/* Seek back to 20. */
	ret = smbc_lseekdir(dhandle, telldir_20);
	torture_assert_int_equal_goto(tctx,
		ret,
		0,
		success,
		done,
		talloc_asprintf(tctx,
			"failed to seek (20) directory handle for '%s'",
			dname));

	/* Read with readdirplus2. */
	direntriesplus_20 = smbc_readdirplus2(dhandle, &st2);
	torture_assert_not_null_goto(
		tctx,
		direntriesplus_20,
		success,
		done,
		"smbc_readdirplus2 (20) failed\n");

	/* Ensure the readdirplus2 and readdirplus names are the same. */
	ret = strcmp(direntries_20->name, direntriesplus_20->name);
	torture_assert_goto(
		tctx,
		ret == 0,
		success,
		done,
		talloc_asprintf(tctx,
				"after seek (20) readdirplus2 name mismatch "
				"file %s - got %s\n",
				dirent_20->name,
				direntries_20->name));

	/* Ensure doing stat gets the same data. */
	plus2_stat_path = talloc_asprintf(tctx,
				"%s/%s",
				dname,
				direntriesplus_20->name);
	torture_assert_not_null_goto(
		tctx,
		plus2_stat_path,
		success,
		done,
		"talloc fail\n");

	ret = smbc_stat(plus2_stat_path, &st);
	torture_assert_int_equal_goto(tctx,
		ret,
		0,
		success,
		done,
		talloc_asprintf(tctx,
			"failed to stat file '%s'",
			plus2_stat_path));

	torture_assert_int_equal(tctx,
		st.st_ino,
		st2.st_ino,
		talloc_asprintf(tctx,
			"file %s mismatched ino value "
			"stat got %"PRIx64" readdirplus2 got %"PRIx64"" ,
			plus2_stat_path,
			(uint64_t)st.st_ino,
			(uint64_t)st2.st_ino));

	torture_assert_int_equal(tctx,
		st.st_dev,
		st2.st_dev,
		talloc_asprintf(tctx,
			"file %s mismatched dev value "
			"stat got %"PRIx64" readdirplus2 got %"PRIx64"" ,
			plus2_stat_path,
			(uint64_t)st.st_dev,
			(uint64_t)st2.st_dev));

	ret = smbc_closedir(dhandle);
	torture_assert_int_equal(tctx,
		ret,
		0,
		talloc_asprintf(tctx,
			"failed to close directory handle for '%s'",
			dname));

	dhandle = -1;
	success = true;

  done:

	/* Clean up. */
	if (dhandle != -1) {
		smbc_closedir(dhandle);
	}
	for (i = 0; i < 100; i++) {
		if (full_filename[i] != NULL) {
			smbc_unlink(full_filename[i]);
		}
	}
	if (dname != NULL) {
		smbc_rmdir(dname);
	}

	smbc_free_context(ctx, 1);

	return success;
}

#ifndef SMBC_FILE_MODE
#define SMBC_FILE_MODE (S_IFREG | 0444)
#endif

static bool torture_libsmbclient_readdirplus2(struct torture_context *tctx)
{
	SMBCCTX *ctx = NULL;
	int dhandle = -1;
	int fhandle = -1;
	bool found = false;
	bool success = false;
	const char *filename = NULL;
	struct stat st2 = {0};
	struct stat st = {0};
	int ret;
	const char *smburl = torture_setting_string(tctx, "smburl", NULL);

	if (smburl == NULL) {
		torture_fail(tctx,
			"option --option=torture:smburl="
			"smb://user:password@server/share missing\n");
	}

	torture_assert_goto(tctx, torture_libsmbclient_init_context(tctx, &ctx), success, done, "");
	smbc_set_context(ctx);

	filename = talloc_asprintf(tctx,
			"%s/test_readdirplus.txt",
			smburl);
	if (filename == NULL) {
		torture_fail_goto(tctx, done, "talloc fail\n");
	}

	/* Ensure the file doesn't exist. */
	smbc_unlink(filename);

	/* Create it. */
	fhandle = smbc_creat(filename, 0666);
	if (fhandle < 0) {
		torture_fail_goto(tctx,
			done,
			talloc_asprintf(tctx,
				"failed to create file '%s': %s",
				filename,
				strerror(errno)));
	}
	ret = smbc_close(fhandle);
	torture_assert_int_equal_goto(tctx,
		ret,
		0,
		success,
		done,
		talloc_asprintf(tctx,
			"failed to close handle for '%s'",
			filename));

	dhandle = smbc_opendir(smburl);
	if (dhandle < 0) {
		int saved_errno = errno;
		smbc_unlink(filename);
		torture_fail_goto(tctx,
			done,
			talloc_asprintf(tctx,
				"failed to obtain "
				"directory handle for '%s' : %s",
				smburl,
				strerror(saved_errno)));
	}

	/* readdirplus2 to ensure we see the new file. */
	for (;;) {
		const struct libsmb_file_info *exstat =
			smbc_readdirplus2(dhandle, &st2);
		if (exstat == NULL) {
			break;
		}

		if (strcmp(exstat->name, "test_readdirplus.txt") == 0) {
			found = true;
			break;
		}
	}

	if (!found) {
		smbc_unlink(filename);
		torture_fail_goto(tctx,
			done,
			talloc_asprintf(tctx,
				"failed to find file '%s'",
				filename));
	}

	/* Ensure mode is as expected. */
	/*
	 * New file gets SMBC_FILE_MODE plus
	 * archive bit -> S_IXUSR
	 * !READONLY -> S_IWUSR.
	 */
	torture_assert_int_equal_goto(tctx,
		st2.st_mode,
		SMBC_FILE_MODE|S_IXUSR|S_IWUSR,
		success,
		done,
		talloc_asprintf(tctx,
			"file %s st_mode should be 0%o, got 0%o'",
			filename,
			SMBC_FILE_MODE|S_IXUSR|S_IWUSR,
			(unsigned int)st2.st_mode));

	/* Ensure smbc_stat() gets the same data. */
	ret = smbc_stat(filename, &st);
	torture_assert_int_equal_goto(tctx,
		ret,
		0,
		success,
		done,
		talloc_asprintf(tctx,
			"failed to stat file '%s'",
			filename));

	torture_assert_int_equal_goto(tctx,
		st2.st_ino,
		st.st_ino,
		success,
		done,
		talloc_asprintf(tctx,
			"filename '%s' ino mismatch. "
			"From smbc_readdirplus2 = %"PRIx64" "
			"From smbc_stat = %"PRIx64"",
			filename,
			(uint64_t)st2.st_ino,
			(uint64_t)st.st_ino));


	/* Remove it again. */
	smbc_unlink(filename);
	ret = smbc_closedir(dhandle);
	torture_assert_int_equal_goto(tctx,
		ret,
		0,
		success,
		done,
		talloc_asprintf(tctx,
			"failed to close directory handle for '%s'",
			filename));
	success = true;

  done:
	smbc_free_context(ctx, 1);
	return success;
}

bool torture_libsmbclient_configuration(struct torture_context *tctx)
{
	SMBCCTX *ctx;
	bool ok = true;

	ctx = smbc_new_context();
	torture_assert(tctx, ctx, "failed to get new context");
	torture_assert(tctx, smbc_init_context(ctx), "failed to init context");

	torture_comment(tctx, "Testing smbc_(set|get)Debug\n");
	smbc_setDebug(ctx, DEBUGLEVEL);
	torture_assert_int_equal_goto(tctx,
				      smbc_getDebug(ctx),
				      DEBUGLEVEL,
				      ok,
				      done,
				      "failed to set DEBUGLEVEL");

	torture_comment(tctx, "Testing smbc_(set|get)NetbiosName\n");
	smbc_setNetbiosName(ctx, discard_const("torture_netbios"));
	torture_assert_str_equal_goto(tctx,
				      smbc_getNetbiosName(ctx),
				      "torture_netbios",
				      ok,
				      done,
				      "failed to set NetbiosName");

	torture_comment(tctx, "Testing smbc_(set|get)Workgroup\n");
	smbc_setWorkgroup(ctx, discard_const("torture_workgroup"));
	torture_assert_str_equal_goto(tctx,
				      smbc_getWorkgroup(ctx),
				      "torture_workgroup",
				      ok,
				      done,
				      "failed to set Workgroup");

	torture_comment(tctx, "Testing smbc_(set|get)User\n");
	smbc_setUser(ctx, "torture_user");
	torture_assert_str_equal_goto(tctx,
				      smbc_getUser(ctx),
				      "torture_user",
				      ok,
				      done,
				      "failed to set User");

	torture_comment(tctx, "Testing smbc_(set|get)Timeout\n");
	smbc_setTimeout(ctx, 12345);
	torture_assert_int_equal_goto(tctx,
				      smbc_getTimeout(ctx),
				      12345,
				      ok,
				      done,
				      "failed to set Timeout");

done:
	smbc_free_context(ctx, 1);

	return ok;
}

bool torture_libsmbclient_options(struct torture_context *tctx)
{
	SMBCCTX *ctx;
	bool ok = true;

	ctx = smbc_new_context();
	torture_assert(tctx, ctx, "failed to get new context");
	torture_assert(tctx, smbc_init_context(ctx), "failed to init context");

	torture_comment(tctx, "Testing smbc_(set|get)OptionDebugToStderr\n");
	smbc_setOptionDebugToStderr(ctx, true);
	torture_assert_goto(tctx,
			    smbc_getOptionDebugToStderr(ctx),
			    ok,
			    done,
			    "failed to set OptionDebugToStderr");

	torture_comment(tctx, "Testing smbc_(set|get)OptionFullTimeNames\n");
	smbc_setOptionFullTimeNames(ctx, true);
	torture_assert_goto(tctx,
			    smbc_getOptionFullTimeNames(ctx),
			    ok,
			    done,
			    "failed to set OptionFullTimeNames");

	torture_comment(tctx, "Testing smbc_(set|get)OptionOpenShareMode\n");
	smbc_setOptionOpenShareMode(ctx, SMBC_SHAREMODE_DENY_ALL);
	torture_assert_int_equal_goto(tctx,
				      smbc_getOptionOpenShareMode(ctx),
				      SMBC_SHAREMODE_DENY_ALL,
				      ok,
				      done,
				      "failed to set OptionOpenShareMode");

	torture_comment(tctx, "Testing smbc_(set|get)OptionUserData\n");
	smbc_setOptionUserData(ctx, (void *)discard_const("torture_user_data"));
	torture_assert_str_equal_goto(tctx,
				      (const char*)smbc_getOptionUserData(ctx),
				      "torture_user_data",
				      ok,
				      done,
				      "failed to set OptionUserData");

	torture_comment(tctx,
			"Testing smbc_(set|get)OptionSmbEncryptionLevel\n");
	smbc_setOptionSmbEncryptionLevel(ctx, SMBC_ENCRYPTLEVEL_REQUEST);
	torture_assert_int_equal_goto(tctx,
				      smbc_getOptionSmbEncryptionLevel(ctx),
				      SMBC_ENCRYPTLEVEL_REQUEST,
				      ok,
				      done,
				      "failed to set OptionSmbEncryptionLevel");

	torture_comment(tctx, "Testing smbc_(set|get)OptionCaseSensitive\n");
	smbc_setOptionCaseSensitive(ctx, false);
	torture_assert_goto(tctx,
			    !smbc_getOptionCaseSensitive(ctx),
			    ok,
			    done,
			    "failed to set OptionCaseSensitive");

	torture_comment(tctx,
			"Testing smbc_(set|get)OptionBrowseMaxLmbCount\n");
	smbc_setOptionBrowseMaxLmbCount(ctx, 2);
	torture_assert_int_equal_goto(tctx,
				      smbc_getOptionBrowseMaxLmbCount(ctx),
				      2,
				      ok,
				      done,
				      "failed to set OptionBrowseMaxLmbCount");

	torture_comment(tctx,
		       "Testing smbc_(set|get)OptionUrlEncodeReaddirEntries\n");
	smbc_setOptionUrlEncodeReaddirEntries(ctx, true);
	torture_assert_goto(tctx,
			    smbc_getOptionUrlEncodeReaddirEntries(ctx),
			    ok,
			    done,
			    "failed to set OptionUrlEncodeReaddirEntries");

	torture_comment(tctx,
			"Testing smbc_(set|get)OptionOneSharePerServer\n");
	smbc_setOptionOneSharePerServer(ctx, true);
	torture_assert_goto(tctx,
			    smbc_getOptionOneSharePerServer(ctx),
			    ok,
			    done,
			    "failed to set OptionOneSharePerServer");

	torture_comment(tctx, "Testing smbc_(set|get)OptionUseKerberos\n");
	smbc_setOptionUseKerberos(ctx, false);
	torture_assert_goto(tctx,
			    !smbc_getOptionUseKerberos(ctx),
			    ok,
			    done,
			    "failed to set OptionUseKerberos");

	torture_comment(tctx,
			"Testing smbc_(set|get)OptionFallbackAfterKerberos\n");
	smbc_setOptionFallbackAfterKerberos(ctx, false);
	torture_assert_goto(tctx,
			    !smbc_getOptionFallbackAfterKerberos(ctx),
			    ok,
			    done,
			    "failed to set OptionFallbackAfterKerberos");

	torture_comment(tctx,
			"Testing smbc_(set|get)OptionNoAutoAnonymousLogin\n");
	smbc_setOptionNoAutoAnonymousLogin(ctx, true);
	torture_assert_goto(tctx,
			    smbc_getOptionNoAutoAnonymousLogin(ctx),
			    ok,
			    done,
			    "failed to set OptionNoAutoAnonymousLogin");

	torture_comment(tctx, "Testing smbc_(set|get)OptionUseCCache\n");
	smbc_setOptionUseCCache(ctx, true);
	torture_assert_goto(tctx,
			    smbc_getOptionUseCCache(ctx),
			    ok,
			    done,
			    "failed to set OptionUseCCache");

done:
	smbc_free_context(ctx, 1);

	return ok;
}

static bool torture_libsmbclient_set_credentials(struct torture_context *tctx)
{
	SMBCCTX *ctx = NULL;
	struct cli_credentials *creds = NULL;
	const char *realm = NULL;
	char *principal = NULL;
	const char *domain = NULL;
	const char *username = NULL;
	bool ok = true;

	ctx = smbc_new_context();
	torture_assert_not_null_goto(tctx,
				     ctx,
				     ok,
				     out,
				     "Failed to create new context");
	torture_assert_not_null_goto(tctx,
				     smbc_init_context(ctx),
				     ok,
				     out,
				     "Failed to initialize context");

	/* Test UPN format: user@REALM */
	smbc_set_credentials_with_fallback(ctx,
					   NULL,
					   "user@REALM.EXAMPLE.COM",
					   "pass");

	creds = ctx->internal->creds;
	torture_assert_not_null_goto(tctx,
				     creds,
				     ok,
				     out,
				     "Expected creds to be set");

	realm = cli_credentials_get_realm(creds);
	torture_assert_str_equal_goto(tctx,
				      realm,
				      "REALM.EXAMPLE.COM",
				      ok,
				      out,
				      "UPN realm not parsed correctly");

	principal = cli_credentials_get_principal(creds, tctx);
	torture_assert_str_equal_goto(tctx,
				      principal,
				      "user@REALM.EXAMPLE.COM",
				      ok,
				      out,
				      "UPN principal not parsed correctly");

	/* Test DOMAIN\user format */
	smbc_set_credentials_with_fallback(ctx,
					   NULL,
					   "TESTDOMAIN\\testuser",
					   "pass");

	creds = ctx->internal->creds;
	torture_assert_not_null_goto(tctx,
				     creds,
				     ok,
				     out,
				     "Expected creds to be set");

	domain = cli_credentials_get_domain(creds);
	torture_assert_str_equal_goto(tctx,
				      domain,
				      "TESTDOMAIN",
				      ok,
				      out,
				      "domain not parsed correctly");

	username = cli_credentials_get_username(creds);
	torture_assert_str_equal_goto(tctx,
				      username,
				      "testuser",
				      ok,
				      out,
				      "DOMAIN\\user not parsed correctly");

out:
	if (ctx != NULL) {
		smbc_free_context(ctx, 1);
	}

	return ok;
}

static bool torture_libsmbclient_list_shares(struct torture_context *tctx)
{
	const char *smburl = torture_setting_string(tctx, "smburl", NULL);
	struct smbc_dirent *dirent = NULL;
	SMBCCTX *ctx = NULL;
	int dhandle = -1;
	bool ipc_share_found = false;
	bool ok = true;

	if (smburl == NULL) {
		torture_fail(tctx,
			     "option --option=torture:smburl="
			     "smb://user:password@server missing\n");
	}

	ok = torture_libsmbclient_init_context(tctx, &ctx);
	torture_assert_goto(tctx,
			    ok,
			    ok,
			    out,
			    "Failed to init context");
	smbc_set_context(ctx);

	torture_comment(tctx, "Listing: %s\n", smburl);
	dhandle = smbc_opendir(smburl);
	torture_assert_int_not_equal_goto(tctx,
					  dhandle,
					  -1,
					  ok,
					  out,
					  "Failed to open smburl");

	while((dirent = smbc_readdir(dhandle)) != NULL) {
		torture_comment(tctx, "DIR: %s\n", dirent->name);
		torture_assert_not_null_goto(tctx,
					     dirent->name,
					     ok,
					     out,
					     "Failed to read name");

		if (strequal(dirent->name, "IPC$")) {
			ipc_share_found = true;
		}
	}

	torture_assert_goto(tctx,
			    ipc_share_found,
			    ok,
			    out,
			    "Failed to list IPC$ share");

out:
	smbc_closedir(dhandle);
	return ok;
}

static bool torture_libsmbclient_utimes(struct torture_context *tctx)
{
	const char *smburl = torture_setting_string(tctx, "smburl", NULL);
	SMBCCTX *ctx = NULL;
	struct stat st;
	int fhandle, ret;
	struct timeval tbuf[2];
	bool ok;

	if (smburl == NULL) {
		torture_fail(tctx,
			     "option --option=torture:smburl="
			     "smb://user:password@server missing\n");
	}

	ok = torture_libsmbclient_init_context(tctx, &ctx);
	torture_assert(tctx, ok, "Failed to init context");
	smbc_set_context(ctx);

	fhandle = smbc_open(smburl, O_RDWR|O_CREAT, 0644);
	torture_assert_int_not_equal(tctx, fhandle, -1, "smbc_open failed");

	ret = smbc_fstat(fhandle, &st);
	torture_assert_int_not_equal(tctx, ret, -1, "smbc_fstat failed");

	tbuf[0] = convert_timespec_to_timeval(get_atimespec(&st));
	tbuf[1] = convert_timespec_to_timeval(get_mtimespec(&st));

	tbuf[1] = timeval_add(&tbuf[1], 0, 100000); /* 100 msec */

	ret = smbc_utimes(smburl, tbuf);
	torture_assert_int_not_equal(tctx, ret, -1, "smbc_utimes failed");

	ret = smbc_fstat(fhandle, &st);
	torture_assert_int_not_equal(tctx, ret, -1, "smbc_fstat failed");

	torture_assert_int_equal(
		tctx,
		get_mtimensec(&st) / 1000,
		tbuf[1].tv_usec,
		"smbc_utimes did not update msec");

	smbc_close(fhandle);
	smbc_unlink(smburl);
	return true;
}

static bool torture_libsmbclient_noanon_list(struct torture_context *tctx)
{
	const char *smburl = torture_setting_string(tctx, "smburl", NULL);
	struct smbc_dirent *dirent = NULL;
	SMBCCTX *ctx = NULL;
	int dhandle = -1;
	bool ok = true;

	if (smburl == NULL) {
		torture_fail(tctx,
			     "option --option=torture:smburl="
			     "smb://user:password@server missing\n");
	}

	ok = torture_libsmbclient_init_context(tctx, &ctx);
	torture_assert_goto(tctx,
			    ok,
			    ok,
			    out,
			    "Failed to init context");
	torture_comment(tctx,
			"Testing smbc_setOptionNoAutoAnonymousLogin\n");
	smbc_setOptionNoAutoAnonymousLogin(ctx, true);
	smbc_set_context(ctx);

	torture_comment(tctx, "Listing: %s\n", smburl);
	dhandle = smbc_opendir(smburl);
	torture_assert_int_not_equal_goto(tctx,
					  dhandle,
					  -1,
					  ok,
					  out,
					  "Failed to open smburl");

	while((dirent = smbc_readdir(dhandle)) != NULL) {
		torture_comment(tctx, "DIR: %s\n", dirent->name);
		torture_assert_not_null_goto(tctx,
					     dirent->name,
					     ok,
					     out,
					     "Failed to read name");
	}

out:
	smbc_closedir(dhandle);
	return ok;
}

static bool torture_libsmbclient_rename(struct torture_context *tctx)
{
	SMBCCTX *ctx = NULL;
	int fhandle = -1;
	bool success = false;
	const char *filename_src = NULL;
	const char *filename_dst = NULL;
	int ret;
	const char *smburl = torture_setting_string(tctx, "smburl", NULL);

	if (smburl == NULL) {
		torture_fail(tctx,
			"option --option=torture:smburl="
			"smb://user:password@server/share missing\n");
	}

	torture_assert_goto(tctx,
				torture_libsmbclient_init_context(tctx, &ctx),
				success,
				done,
				"");

	smbc_set_context(ctx);

	filename_src = talloc_asprintf(tctx,
			"%s/src",
			smburl);
	if (filename_src == NULL) {
		torture_fail_goto(tctx, done, "talloc fail\n");
	}

	filename_dst = talloc_asprintf(tctx,
			"%s/dst",
			smburl);
	if (filename_dst == NULL) {
		torture_fail_goto(tctx, done, "talloc fail\n");
	}

	/* Ensure the files don't exist. */
	smbc_unlink(filename_src);
	smbc_unlink(filename_dst);

	/* Create them. */
	fhandle = smbc_creat(filename_src, 0666);
	if (fhandle < 0) {
		torture_fail_goto(tctx,
			done,
			talloc_asprintf(tctx,
				"failed to create file '%s': %s",
				filename_src,
				strerror(errno)));
	}
	ret = smbc_close(fhandle);
	torture_assert_int_equal_goto(tctx,
		ret,
		0,
		success,
		done,
		talloc_asprintf(tctx,
			"failed to close handle for '%s'",
			filename_src));

	fhandle = smbc_creat(filename_dst, 0666);
	if (fhandle < 0) {
		torture_fail_goto(tctx,
			done,
			talloc_asprintf(tctx,
				"failed to create file '%s': %s",
				filename_dst,
				strerror(errno)));
	}
	ret = smbc_close(fhandle);
	torture_assert_int_equal_goto(tctx,
		ret,
		0,
		success,
		done,
		talloc_asprintf(tctx,
			"failed to close handle for '%s'",
			filename_dst));

	ret = smbc_rename(filename_src, filename_dst);

	/*
	 * BUG: https://bugzilla.samba.org/show_bug.cgi?id=14938
	 * gives ret == -1, but errno = 0 for overwrite renames
	 * over SMB2.
	 */
	torture_assert_int_equal_goto(tctx,
		ret,
		0,
		success,
		done,
		talloc_asprintf(tctx,
			"smbc_rename '%s' -> '%s' failed with %s\n",
			filename_src,
			filename_dst,
			strerror(errno)));

	/* Remove them again. */
	smbc_unlink(filename_src);
	smbc_unlink(filename_dst);
	success = true;

  done:
	smbc_free_context(ctx, 1);
	return success;
}

static bool torture_libsmbclient_getatr(struct torture_context *tctx)
{
	const char *smburl = torture_setting_string(tctx, "smburl", NULL);
	SMBCCTX *ctx = NULL;
	char *getatr_name = NULL;
	struct stat st = {0};
	bool ok;
	int ret = 0;
	int err = 0;

	if (smburl == NULL) {
		torture_fail(tctx,
			     "option --option=torture:smburl="
			     "smb://user:password@server missing\n");
	}

	ok = torture_libsmbclient_init_context(tctx, &ctx);
	torture_assert(tctx, ok, "Failed to init context");
	smbc_set_context(ctx);

	getatr_name = talloc_asprintf(tctx,
			"%s/noexist",
			smburl);
	if (getatr_name == NULL) {
		torture_result(tctx,
			       TORTURE_FAIL,
			       __location__": %s",
			       "talloc fail\n");
		return false;
	}
	/* Ensure the file doesn't exist. */
	smbc_unlink(getatr_name);
	/*
	 * smbc_stat() internally uses SMBC_getatr().
	 * Make sure doing getatr on a non-existent file gives
	 * an error of -1, errno = ENOENT.
	 */

	ret = smbc_stat(getatr_name, &st);
	if (ret == -1) {
		err = errno;
	}
	torture_assert_int_equal(tctx,
				 ret,
				 -1,
				 talloc_asprintf(tctx,
					"smbc_stat on '%s' should "
					"get -1, got %d\n",
					getatr_name,
					ret));
	torture_assert_int_equal(tctx,
				 err,
				 ENOENT,
				 talloc_asprintf(tctx,
					"smbc_stat on '%s' should "
					"get errno = ENOENT, got %s\n",
					getatr_name,
					strerror(err)));
	return true;
}

static bool torture_libsmbclient_getxattr(struct torture_context *tctx)
{
	const char *smburl = torture_setting_string(tctx, "smburl", NULL);
	int fhandle = -1;
	SMBCCTX *ctx = NULL;
	char *getxattr_name = NULL;
	char value[4096];
	bool ok = false;
	int ret = -1;

	if (smburl == NULL) {
		torture_fail(tctx,
			     "option --option=torture:smburl="
			     "smb://user:password@server missing\n");
	}

	ok = torture_libsmbclient_init_context(tctx, &ctx);
	torture_assert(tctx, ok, "Failed to init context");
	smbc_set_context(ctx);

	getxattr_name = talloc_asprintf(tctx,
			"%s/getxattr",
			smburl);
	if (getxattr_name == NULL) {
		torture_result(tctx,
			       TORTURE_FAIL,
			       __location__": %s",
			       "talloc fail\n");
		return false;
	}
	/* Ensure the file doesn't exist. */
	smbc_unlink(getxattr_name);

	/* Create testfile. */
	fhandle = smbc_creat(getxattr_name, 0666);
	if (fhandle < 0) {
		torture_fail_goto(tctx,
			done,
			talloc_asprintf(tctx,
				"failed to create file '%s': %s",
				getxattr_name,
				strerror(errno)));
	}
	ret = smbc_close(fhandle);
	torture_assert_int_equal_goto(tctx,
		ret,
		0,
		ok,
		done,
		talloc_asprintf(tctx,
			"failed to close handle for '%s'",
			getxattr_name));

	/*
	 * Ensure getting a non-existent attribute returns -1.
	 */
	ret = smbc_getxattr(getxattr_name, "foobar", value, sizeof(value));
	torture_assert_int_equal_goto(tctx,
		ret,
		-1,
		ok,
		done,
		talloc_asprintf(tctx,
			"smbc_getxattr(foobar) on '%s' should "
			"get -1, got %d\n",
			getxattr_name,
			ret));

	/*
	 * Ensure getting a valid attribute computes its size.
	 */
	ret = smbc_getxattr(getxattr_name, "system.*", NULL, 0);
	torture_assert_goto(tctx,
		ret >= 0,
		ok,
		done,
		talloc_asprintf(tctx,
			"smbc_getxattr(foobar, NULL) on '%s' should "
			"get >=0, got %d\n",
			getxattr_name,
			ret));

	/*
	 * Ensure getting a valid attribute returns its size.
	 */
	ret = smbc_getxattr(getxattr_name, "system.*", value, sizeof(value));
	torture_assert_goto(tctx,
		ret >= 0,
		ok,
		done,
		talloc_asprintf(tctx,
			"smbc_getxattr(foobar, value) on '%s' should "
			"get >=0, got %d\n",
			getxattr_name,
			ret));

	ok = true;

  done:

	smbc_unlink(getxattr_name);
	smbc_free_context(ctx, 1);
	return ok;
}

static bool torture_libsmbclient_init_posix_context(struct torture_context *tctx,
						     SMBCCTX **ctx_p)
{
	SMBCCTX *ctx = NULL;
	SMBCCTX *p = NULL;
	bool ok = true;

	ctx = smbc_new_context();
	torture_assert_not_null_goto(
		tctx, ctx, ok, out, "Failed to create new context");

	torture_comment(tctx, "Enabling POSIX extensions\n");
	smbc_setOptionPosixExtensions(ctx, true);

	p = smbc_init_context(ctx);
	torture_assert_not_null_goto(tctx,
				     p,
				     ok,
				     out,
				     "Failed to initialize context");

	smbc_setFunctionAuthData(ctx, auth_callback);
	smbc_set_context(ctx);

	*ctx_p = ctx;

out:
	if (!ok) {
		smbc_free_context(ctx, true);
	}

	return ok;
}

static bool torture_libsmbclient_posix_extensions(struct torture_context *tctx)
{
	const char *smburl = torture_setting_string(tctx, "smburl", NULL);
	int fhandle = -1;
	SMBCCTX *ctx = NULL;
	char *posix_name = NULL;
	char buf[2] = {'\0'};
	bool ok = false;
	int ret = -1;

	if (smburl == NULL) {
		torture_fail(tctx,
			     "option --option=torture:smburl="
			     "smb://user:password@server missing\n");
	}

	torture_assert_goto(tctx,
			    torture_libsmbclient_init_posix_context(tctx, &ctx),
			    ok,
			    done,
			    "Failed to init POSIX context");

	posix_name = talloc_asprintf(tctx,
				     "%s/posix_test_original.txt",
				     smburl);
	torture_assert_not_null_goto(tctx,
				     posix_name,
				     ok,
				     done,
				     "talloc_asprintf failed\n");

	/* Open the test file */
	fhandle = smbc_open(posix_name, O_RDONLY, 0);
	torture_assert_goto(tctx,
			    fhandle >= 0,
			    ok,
			    done,
			    talloc_asprintf(tctx,
					    "failed to open file '%s': %s",
					    posix_name,
					    strerror(errno)));

	/*
	 * Check if we got POSIX extensions by reading the
	 * "posix.attr.enabled" extended attribute.
	 * This should return "1" if POSIX extensions are active.
	 */
	torture_comment(tctx, "Checking for POSIX extensions\n");
	ret = smbc_fgetxattr(fhandle, "posix.attr.enabled", buf, sizeof(buf));

	torture_assert_int_equal_goto(
		tctx,
		ret,
		1,
		ok,
		done,
		"smbc_fgetxattr(posix.attr.enabled) failed");

	torture_assert_str_equal_goto(
		tctx, buf, "1", ok, done, "POSIX extensions not enabled.");
	torture_comment(tctx, "POSIX extensions are enabled!\n");

	ok = true;

done:
	if (fhandle >= 0) {
		smbc_close(fhandle);
	}
	if (ctx != NULL) {
		smbc_free_context(ctx, 1);
	}

	return ok;
}

static bool torture_libsmbclient_posix_hardlinks(struct torture_context *tctx)
{
	const char *smburl = torture_setting_string(tctx, "smburl", NULL);
	int fhandle = -1;
	SMBCCTX *ctx = NULL;
	char *posix_name = NULL;
	char buf[sizeof(struct stat) + 4]; /* +4 bytes for DOS attributes */
	struct stat st_posix;
	bool ok = false;
	int ret = -1;

	if (smburl == NULL) {
		torture_fail(tctx,
			     "option --option=torture:smburl="
			     "smb://user:password@server missing\n");
	}

	torture_assert_goto(tctx,
			    torture_libsmbclient_init_posix_context(tctx, &ctx),
			    ok,
			    done,
			    "Failed to init POSIX context");

	posix_name = talloc_asprintf(tctx,
				     "%s/posix_test_original.txt",
				     smburl);
	torture_assert_not_null_goto(tctx,
				     posix_name,
				     ok,
				     done,
				     "talloc_asprintf failed\n");

	/* Open the file with hardlinks */
	fhandle = smbc_open(posix_name, O_RDONLY, 0);
	torture_assert_goto(tctx,
			    fhandle >= 0,
			    ok,
			    done,
			    talloc_asprintf(tctx,
					    "failed to open file '%s': %s",
					    posix_name,
					    strerror(errno)));

	/*
	 * Get POSIX stat information including hardlink count
	 */
	torture_comment(tctx, "Getting POSIX stat info for hardlink test\n");
	ret = smbc_fgetxattr(fhandle,
			     "smb311_posix.statinfo",
			     buf,
			     sizeof(buf));

	torture_assert_int_equal_goto(
		tctx,
		ret,
		sizeof(struct stat),
		ok,
		done,
		talloc_asprintf(tctx,
				"smbc_fgetxattr(smb311_posix.statinfo) "
				"returned %d, expected %zu",
				ret,
				sizeof(struct stat)));

	memcpy(&st_posix, buf, sizeof(struct stat));

	torture_comment(tctx,
			"File has %lu hardlinks\n",
			(unsigned long)st_posix.st_nlink);

	torture_assert_int_equal_goto(
		tctx,
		st_posix.st_nlink,
		3,
		ok,
		done,
		talloc_asprintf(tctx,
				"Expected 3 hardlinks, got %lu",
				(unsigned long)st_posix.st_nlink));

	ok = true;

done:
	if (fhandle >= 0) {
		smbc_close(fhandle);
	}
	if (ctx != NULL) {
		smbc_free_context(ctx, 1);
	}

	return ok;
}

NTSTATUS torture_libsmbclient_init(TALLOC_CTX *ctx)
{
	struct torture_suite *suite;

	suite = torture_suite_create(ctx, "libsmbclient");

	torture_suite_add_simple_test(suite, "version", torture_libsmbclient_version);
	torture_suite_add_simple_test(suite, "initialize", torture_libsmbclient_initialize);
	torture_suite_add_simple_test(suite, "configuration", torture_libsmbclient_configuration);
	torture_suite_add_simple_test(suite, "setConfiguration", torture_libsmbclient_setConfiguration);
	torture_suite_add_simple_test(suite, "options", torture_libsmbclient_options);
	torture_suite_add_simple_test(suite,
		"set_credentials",
		torture_libsmbclient_set_credentials);
	torture_suite_add_simple_test(suite, "opendir", torture_libsmbclient_opendir);
	torture_suite_add_simple_test(suite, "list_shares", torture_libsmbclient_list_shares);
	torture_suite_add_simple_test(suite, "readdirplus",
		torture_libsmbclient_readdirplus);
	torture_suite_add_simple_test(suite, "readdirplus_seek",
		torture_libsmbclient_readdirplus_seek);
	torture_suite_add_simple_test(suite, "readdirplus2",
		torture_libsmbclient_readdirplus2);
	torture_suite_add_simple_test(
		suite, "utimes", torture_libsmbclient_utimes);
	torture_suite_add_simple_test(
		suite, "noanon_list", torture_libsmbclient_noanon_list);
	torture_suite_add_simple_test(suite,
					"rename",
					torture_libsmbclient_rename);
	torture_suite_add_simple_test(suite, "getatr",
		torture_libsmbclient_getatr);
	torture_suite_add_simple_test(suite, "getxattr",
		torture_libsmbclient_getxattr);
	torture_suite_add_simple_test(suite, "posix_extensions",
		torture_libsmbclient_posix_extensions);
	torture_suite_add_simple_test(suite, "posix_hardlinks",
		torture_libsmbclient_posix_hardlinks);

	suite->description = talloc_strdup(suite, "libsmbclient interface tests");

	torture_register_suite(ctx, suite);

	return NT_STATUS_OK;
}
