/*
   Unix SMB/CIFS implementation.
   SMB read/write torture tester
   Copyright (C) Andrew Tridgell 1997-2003
   Copyright (C) Jelmer Vernooij 2006
   Copyright (C) David Mulder 2019

   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 "includes.h"
#include "torture/smbtorture.h"
#include "libcli/smb2/smb2.h"
#include "libcli/smb2/smb2_calls.h"
#include "torture/torture.h"
#include "torture/util.h"
#include "torture/smb2/proto.h"

#define CHECK_STATUS(_status, _expected) \
	torture_assert_ntstatus_equal_goto(torture, _status, _expected, \
		 ret, done, "Incorrect status")

#define CHECK_VALUE(v, correct) \
	torture_assert_int_equal_goto(torture, v, correct, \
		 ret, done, "Incorrect value")

#define FNAME "smb2_writetest.dat"

static bool run_smb2_readwritetest(struct torture_context *tctx,
				   struct smb2_tree *t1, struct smb2_tree *t2)
{
	const char *lockfname = "torture2.lck";
	struct smb2_create f1 = {0};
	struct smb2_create f2 = {0};
	struct smb2_handle h1 = {{0}};
	struct smb2_handle h2 = {{0}};
	int i;
	uint8_t buf[131072];
	bool correct = true;
	NTSTATUS status;
	int ret = 0;

	ret = smb2_deltree(t1, lockfname);
	torture_assert(tctx, ret != -1, "unlink failed");

	f1.in.desired_access = SEC_FILE_ALL;
	f1.in.share_access = NTCREATEX_SHARE_ACCESS_READ |
			     NTCREATEX_SHARE_ACCESS_WRITE;
	f1.in.create_disposition = FILE_CREATE;
	f1.in.fname = lockfname;

	status = smb2_create(t1, tctx, &f1);
	torture_assert_ntstatus_ok_goto(tctx, status, correct, done,
		talloc_asprintf(tctx, "first open read/write of %s failed (%s)",
		lockfname, nt_errstr(status)));
	h1 = f1.out.file.handle;

	f2.in.desired_access = SEC_FILE_READ_DATA;
	f2.in.share_access = NTCREATEX_SHARE_ACCESS_READ |
			     NTCREATEX_SHARE_ACCESS_WRITE;
	f2.in.create_disposition = FILE_OPEN;
	f2.in.fname = lockfname;

	status = smb2_create(t2, tctx, &f2);
	torture_assert_ntstatus_ok_goto(tctx, status, correct, done,
		talloc_asprintf(tctx, "second open read-only of %s failed (%s)",
		lockfname, nt_errstr(status)));
	h2 = f2.out.file.handle;

	torture_comment(tctx, "Checking data integrity over %d ops\n",
			torture_numops);

	for (i = 0; i < torture_numops; i++) {
		struct smb2_write w = {0};
		struct smb2_read r = {0};
		size_t buf_size = ((unsigned int)random()%(sizeof(buf)-1))+ 1;

		if (i % 10 == 0) {
			if (torture_setting_bool(tctx, "progress", true)) {
				torture_comment(tctx, "%d\r", i); fflush(stdout);
			}
		}

		generate_random_buffer(buf, buf_size);

		w.in.file.handle = h1;
		w.in.offset = 0;
		w.in.data.data = buf;
		w.in.data.length = buf_size;

		status = smb2_write(t1, &w);
		if (!NT_STATUS_IS_OK(status) || w.out.nwritten != buf_size) {
			torture_comment(tctx, "write failed (%s)\n",
					nt_errstr(status));
			torture_result(tctx, TORTURE_FAIL,
				       "wrote %d, expected %d\n",
				       (int)w.out.nwritten, (int)buf_size);
			correct = false;
			goto done;
		}

		r.in.file.handle = h2;
		r.in.offset = 0;
		r.in.length = buf_size;
		status = smb2_read(t2, tctx, &r);
		if (!NT_STATUS_IS_OK(status) || r.out.data.length != buf_size) {
			torture_comment(tctx, "read failed (%s)\n",
					nt_errstr(status));
			torture_result(tctx, TORTURE_FAIL,
				       "read %d, expected %d\n",
				       (int)r.out.data.length, (int)buf_size);
			correct = false;
			goto done;
		}

		torture_assert_mem_equal_goto(tctx, r.out.data.data, buf,
			buf_size, correct, done, "read/write compare failed\n");
	}

	status = smb2_util_close(t2, h2);
	torture_assert_ntstatus_ok_goto(tctx, status, correct, done,
		talloc_asprintf(tctx, "close failed (%s)", nt_errstr(status)));
	ZERO_STRUCT(h2);

	status = smb2_util_close(t1, h1);
	torture_assert_ntstatus_ok_goto(tctx, status, correct, done,
		talloc_asprintf(tctx, "close failed (%s)", nt_errstr(status)));
	ZERO_STRUCT(h1);

done:
	if (!smb2_util_handle_empty(h2)) {
		smb2_util_close(t2, h2);
	}
	if (!smb2_util_handle_empty(h1)) {
		smb2_util_close(t1, h1);
	}

	status = smb2_util_unlink(t1, lockfname);
	if (!NT_STATUS_IS_OK(status)) {
		torture_comment(tctx, "unlink failed (%s)", nt_errstr(status));
	}

	return correct;
}


static bool run_smb2_wrap_readwritetest(struct torture_context *tctx,
					struct smb2_tree *tree1,
					struct smb2_tree *tree2)
{
	return run_smb2_readwritetest(tctx, tree1, tree1);
}

static bool test_rw_invalid(struct torture_context *torture, struct smb2_tree *tree)
{
	bool ret = true;
	NTSTATUS status;
	struct smb2_handle h;
	uint8_t buf[64*1024];
	struct smb2_read rd;
	struct smb2_write w = {0};
	union smb_setfileinfo sfinfo;
	TALLOC_CTX *tmp_ctx = talloc_new(tree);

	ZERO_STRUCT(buf);

	smb2_util_unlink(tree, FNAME);

	status = torture_smb2_testfile(tree, FNAME, &h);
	CHECK_STATUS(status, NT_STATUS_OK);

	/* set delete-on-close */
	ZERO_STRUCT(sfinfo);
	sfinfo.generic.level = RAW_SFILEINFO_DISPOSITION_INFORMATION;
	sfinfo.disposition_info.in.delete_on_close = 1;
	sfinfo.generic.in.file.handle = h;
	status = smb2_setinfo_file(tree, &sfinfo);
	CHECK_STATUS(status, NT_STATUS_OK);

	status = smb2_util_write(tree, h, buf, 0, ARRAY_SIZE(buf));
	CHECK_STATUS(status, NT_STATUS_OK);

	ZERO_STRUCT(rd);
	rd.in.file.handle = h;
	rd.in.length = 10;
	rd.in.offset = 0;
	rd.in.min_count = 1;

	status = smb2_read(tree, tmp_ctx, &rd);
	CHECK_STATUS(status, NT_STATUS_OK);
	CHECK_VALUE(rd.out.data.length, 10);

	rd.in.min_count = 0;
	rd.in.length = 10;
	rd.in.offset = sizeof(buf);
	status = smb2_read(tree, tmp_ctx, &rd);
	CHECK_STATUS(status, NT_STATUS_END_OF_FILE);

	rd.in.min_count = 0;
	rd.in.length = 0;
	rd.in.offset = sizeof(buf);
	status = smb2_read(tree, tmp_ctx, &rd);
	CHECK_STATUS(status, NT_STATUS_OK);
	CHECK_VALUE(rd.out.data.length, 0);

	rd.in.min_count = 0;
	rd.in.length = 1;
	rd.in.offset = INT64_MAX - 1;
	status = smb2_read(tree, tmp_ctx, &rd);
	CHECK_STATUS(status, NT_STATUS_END_OF_FILE);

	rd.in.min_count = 0;
	rd.in.length = 0;
	rd.in.offset = INT64_MAX;
	status = smb2_read(tree, tmp_ctx, &rd);
	CHECK_STATUS(status, NT_STATUS_OK);
	CHECK_VALUE(rd.out.data.length, 0);

	rd.in.min_count = 0;
	rd.in.length = 1;
	rd.in.offset = INT64_MAX;
	status = smb2_read(tree, tmp_ctx, &rd);
	CHECK_STATUS(status, NT_STATUS_INVALID_PARAMETER);

	rd.in.min_count = 0;
	rd.in.length = 0;
	rd.in.offset = (uint64_t)INT64_MAX + 1;
	status = smb2_read(tree, tmp_ctx, &rd);
	CHECK_STATUS(status, NT_STATUS_INVALID_PARAMETER);

	rd.in.min_count = 0;
	rd.in.length = 0;
	rd.in.offset = (uint64_t)INT64_MIN;
	status = smb2_read(tree, tmp_ctx, &rd);
	CHECK_STATUS(status, NT_STATUS_INVALID_PARAMETER);

	rd.in.min_count = 0;
	rd.in.length = 0;
	rd.in.offset = (uint64_t)(int64_t)-1;
	status = smb2_read(tree, tmp_ctx, &rd);
	CHECK_STATUS(status, NT_STATUS_INVALID_PARAMETER);

	rd.in.min_count = 0;
	rd.in.length = 0;
	rd.in.offset = (uint64_t)(int64_t)-2;
	status = smb2_read(tree, tmp_ctx, &rd);
	CHECK_STATUS(status, NT_STATUS_INVALID_PARAMETER);

	rd.in.min_count = 0;
	rd.in.length = 0;
	rd.in.offset = (uint64_t)(int64_t)-3;
	status = smb2_read(tree, tmp_ctx, &rd);
	CHECK_STATUS(status, NT_STATUS_INVALID_PARAMETER);

	w.in.file.handle = h;
	w.in.offset = (int64_t)-1;
	w.in.data.data = buf;
	w.in.data.length = ARRAY_SIZE(buf);

	status = smb2_write(tree, &w);
	CHECK_STATUS(status, NT_STATUS_INVALID_PARAMETER);

	w.in.file.handle = h;
	w.in.offset = (int64_t)-2;
	w.in.data.data = buf;
	w.in.data.length = ARRAY_SIZE(buf);

	status = smb2_write(tree, &w);
	CHECK_STATUS(status, NT_STATUS_INVALID_PARAMETER);

	w.in.file.handle = h;
	w.in.offset = INT64_MIN;
	w.in.data.data = buf;
	w.in.data.length = 1;

	status = smb2_write(tree, &w);
	CHECK_STATUS(status, NT_STATUS_INVALID_PARAMETER);

	w.in.file.handle = h;
	w.in.offset = INT64_MIN;
	w.in.data.data = buf;
	w.in.data.length = 0;
	status = smb2_write(tree, &w);
	CHECK_STATUS(status, NT_STATUS_INVALID_PARAMETER);

	w.in.file.handle = h;
	w.in.offset = INT64_MAX;
	w.in.data.data = buf;
	w.in.data.length = 0;
	status = smb2_write(tree, &w);
	CHECK_STATUS(status, NT_STATUS_OK);
	CHECK_VALUE(w.out.nwritten, 0);

	w.in.file.handle = h;
	w.in.offset = INT64_MAX;
	w.in.data.data = buf;
	w.in.data.length = 1;
	status = smb2_write(tree, &w);
	CHECK_STATUS(status, NT_STATUS_INVALID_PARAMETER);

	w.in.file.handle = h;
	w.in.offset = (uint64_t)INT64_MAX + 1;
	w.in.data.data = buf;
	w.in.data.length = 0;
	status = smb2_write(tree, &w);
	CHECK_STATUS(status, NT_STATUS_INVALID_PARAMETER);

	w.in.file.handle = h;
	w.in.offset = 0xfffffff0000; /* MAXFILESIZE */
	w.in.data.data = buf;
	w.in.data.length = 1;
	status = smb2_write(tree, &w);
	CHECK_STATUS(status, NT_STATUS_INVALID_PARAMETER);

	w.in.file.handle = h;
	w.in.offset = 0xfffffff0000 - 1; /* MAXFILESIZE - 1 */
	w.in.data.data = buf;
	w.in.data.length = 1;
	status = smb2_write(tree, &w);
	if (TARGET_IS_SAMBA3(torture) || TARGET_IS_SAMBA4(torture)) {
		CHECK_STATUS(status, NT_STATUS_OK);
		CHECK_VALUE(w.out.nwritten, 1);
	} else {
		CHECK_STATUS(status, NT_STATUS_DISK_FULL);
	}

	w.in.file.handle = h;
	w.in.offset = 0xfffffff0000; /* MAXFILESIZE */
	w.in.data.data = buf;
	w.in.data.length = 0;
	status = smb2_write(tree, &w);
	CHECK_STATUS(status, NT_STATUS_OK);
	CHECK_VALUE(w.out.nwritten, 0);

done:
	talloc_free(tmp_ctx);
	return ret;
}

static bool test_append(struct torture_context *tctx,
			struct smb2_tree *tree)
{
	struct smb2_handle h = {};
	uint8_t buf[1] = {};
	struct smb2_create c = {};
	union smb_fileinfo finfo = {};
	off_t off;
	uint64_t expected_size = 0;
	NTSTATUS status;
	bool ret = true;

	smb2_util_unlink(tree, FNAME);

	torture_comment(tctx, "Create file with 1 byte\n");

	status = torture_smb2_testfile(tree, FNAME, &h);
	torture_assert_ntstatus_ok_goto(tctx, status, ret, done,
					"Incorrect status\n");

	expected_size = 0;

	status = smb2_util_write(tree, h, buf, 0, ARRAY_SIZE(buf));
	torture_assert_ntstatus_ok_goto(tctx, status, ret, done,
					"Incorrect status\n");
	expected_size += ARRAY_SIZE(buf);

	status = smb2_util_close(tree, h);
	torture_assert_ntstatus_ok_goto(tctx, status, ret, done,
					"Incorrect status\n");
	ZERO_STRUCT(h);

	torture_comment(tctx, "Open file with SEC_FILE_APPEND_DATA"
			"|SEC_STD_SYNCHRONIZE\n");

	c.in.desired_access = SEC_FILE_APPEND_DATA | SEC_STD_SYNCHRONIZE;
	c.in.create_disposition = NTCREATEX_DISP_OPEN;
	c.in.share_access =
		NTCREATEX_SHARE_ACCESS_DELETE|
		NTCREATEX_SHARE_ACCESS_READ|
		NTCREATEX_SHARE_ACCESS_WRITE;
	c.in.fname = FNAME;

	status = smb2_create(tree, tree, &c);
	torture_assert_ntstatus_ok_goto(tctx, status, ret, done,
					"Incorrect status\n");

	h = c.out.file.handle;

	torture_comment(tctx, "Write one byte at offset 0 and verify "
			"written data was not appended but written at "
			"requested offset\n");

	status = smb2_util_write(tree, h, buf, 0, ARRAY_SIZE(buf));
	torture_assert_ntstatus_ok_goto(tctx, status, ret, done,
					"Incorrect status\n");

	status = smb2_util_close(tree, h);
	torture_assert_ntstatus_ok_goto(tctx, status, ret, done,
					"Incorrect status\n");
	ZERO_STRUCT(h);

	status = torture_smb2_testfile(tree, FNAME, &h);
	torture_assert_ntstatus_ok_goto(tctx, status, ret, done,
					"Incorrect status\n");

	finfo.generic.level = RAW_FILEINFO_ALL_INFORMATION;
	finfo.generic.in.file.handle = h;
	status = smb2_getinfo_file(tree, tree, &finfo);
	torture_assert_ntstatus_ok_goto(tctx, status, ret, done,
					"Incorrect status\n");

	torture_assert_u64_equal_goto(tctx, finfo.all_info.out.size,
				      expected_size,
				      ret, done, "wrong size");

	status = smb2_util_close(tree, h);
	torture_assert_ntstatus_ok_goto(tctx, status, ret, done,
					"Incorrect status\n");
	ZERO_STRUCT(h);

	torture_comment(tctx, "Open file in write-data only mode "
			"and check if we can append\n");

	c.in.desired_access = SEC_FILE_WRITE_DATA | SEC_FILE_READ_ATTRIBUTE;
	c.in.create_disposition = NTCREATEX_DISP_OPEN;
	c.in.share_access =
		NTCREATEX_SHARE_ACCESS_DELETE|
		NTCREATEX_SHARE_ACCESS_READ|
		NTCREATEX_SHARE_ACCESS_WRITE;
	c.in.fname = FNAME;

	status = smb2_create(tree, tree, &c);
	torture_assert_ntstatus_ok_goto(tctx, status, ret, done,
					"Incorrect status\n");
	h = c.out.file.handle;

	off = 1000;

	status = smb2_util_write(tree, h, buf, off, ARRAY_SIZE(buf));
	torture_assert_ntstatus_ok_goto(tctx, status, ret, done,
					"Incorrect status\n");

	expected_size = off + ARRAY_SIZE(buf);

	finfo.generic.level = RAW_FILEINFO_ALL_INFORMATION;
	finfo.generic.in.file.handle = h;
	status = smb2_getinfo_file(tree, tree, &finfo);
	torture_assert_ntstatus_ok_goto(tctx, status, ret, done,
					"Incorrect status\n");

	torture_assert_u64_equal_goto(tctx, finfo.all_info.out.size,
				      expected_size,
				      ret, done, "wrong size");

	status = smb2_util_close(tree, h);
	torture_assert_ntstatus_ok_goto(tctx, status, ret, done,
					"Incorrect status\n");
	ZERO_STRUCT(h);

done:
	if (!smb2_util_handle_empty(h)) {
		smb2_util_close(tree, h);
	}
	smb2_util_unlink(tree, FNAME);
	return ret;
}


struct torture_suite *torture_smb2_readwrite_init(TALLOC_CTX *ctx)
{
	struct torture_suite *suite = torture_suite_create(ctx, "rw");

	torture_suite_add_2smb2_test(suite, "rw1", run_smb2_readwritetest);
	torture_suite_add_2smb2_test(suite, "rw2", run_smb2_wrap_readwritetest);
	torture_suite_add_1smb2_test(suite, "invalid", test_rw_invalid);
	torture_suite_add_1smb2_test(suite, "append", test_append);

	suite->description = talloc_strdup(suite, "SMB2 Samba4 Read/Write");

	return suite;
}
