/*
 * Store streams in a separate subdirectory
 *
 * Copyright (C) Volker Lendecke, 2007
 *
 * 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 "smbd/smbd.h"
#include "system/filesys.h"
#include "source3/smbd/dir.h"

#undef DBGC_CLASS
#define DBGC_CLASS DBGC_VFS

struct streams_depot_config_data {
	const char *directory;
	bool check_valid;
	bool delete_lost;
};

/*
 * Excerpt from a mail from tridge:
 *
 * Volker, what I'm thinking of is this:
 * /mount-point/.streams/XX/YY/aaaa.bbbb/namedstream1
 * /mount-point/.streams/XX/YY/aaaa.bbbb/namedstream2
 *
 * where XX/YY is a 2 level hash based on the fsid/inode. "aaaa.bbbb"
 * is the fsid/inode. "namedstreamX" is a file named after the stream
 * name.
 */

static uint32_t hash_fn(DATA_BLOB key)
{
	uint32_t value;	/* Used to compute the hash value.  */
	uint32_t i;	/* Used to cycle through random values. */

	/* Set the initial value from the key size. */
	for (value = 0x238F13AF * key.length, i=0; i < key.length; i++)
		value = (value + (key.data[i] << (i*5 % 24)));

	return (1103515243 * value + 12345);
}

/*
 * With the hashing scheme based on the inode we need to protect against
 * streams showing up on files with re-used inodes. This can happen if we
 * create a stream directory from within Samba, and a local process or NFS
 * client deletes the file without deleting the streams directory. When the
 * inode is re-used and the stream directory is still around, the streams in
 * there would be show up as belonging to the new file.
 *
 * There are several workarounds for this, probably the easiest one is on
 * systems which have a true birthtime stat element: When the file has a later
 * birthtime than the streams directory, then we have to recreate the
 * directory.
 *
 * The other workaround is to somehow mark the file as generated by Samba with
 * something that a NFS client would not do. The closest one is a special
 * xattr value being set. On systems which do not support xattrs, it might be
 * an option to put in a special ACL entry for a non-existing group.
 */

/*
 * Return the root of the stream directory. Can be
 * external to the share definition but by default
 * is "handle->conn->connectpath/.streams".
 *
 * Note that this is an *absolute* path, starting
 * with '/', so the dirfsp being used in the
 * calls below isn't looked at.
 */

static char *stream_rootdir(vfs_handle_struct *handle,
			    TALLOC_CTX *ctx)
{
	const struct loadparm_substitution *lp_sub =
		loadparm_s3_global_substitution();
	char *tmp;

	tmp = talloc_asprintf(ctx,
			      "%s/.streams",
			      handle->conn->connectpath);
	if (tmp == NULL) {
		errno = ENOMEM;
		return NULL;
	}

	return lp_parm_substituted_string(ctx,
					  lp_sub,
					  SNUM(handle->conn),
					  "streams_depot",
					  "directory",
					  tmp);
}

static int streams_depot_mkdir_pathref(TALLOC_CTX *mem_ctx,
				       vfs_handle_struct *handle,
				       struct files_struct *dirfsp,
				       const char *relname,
				       mode_t mode,
				       struct smb_filename **_result)
{
	struct smb_filename *dirname = dirfsp->fsp_name;
	struct smb_filename *result = NULL;
	NTSTATUS status;
	int ret;

	result = synthetic_smb_fname(
		mem_ctx, relname, NULL, NULL, dirname->twrp, dirname->flags);
	if (result == NULL) {
		return ENOMEM;
	}

	ret = SMB_VFS_NEXT_MKDIRAT(handle, dirfsp, result, mode);
	if ((ret != 0) && (errno != EEXIST)) {
		ret = errno;
		TALLOC_FREE(result);
		return ret;
	}

	status = openat_pathref_fsp_lcomp(dirfsp, result, UCF_POSIX_PATHNAMES);
	if (!NT_STATUS_IS_OK(status)) {
		ret = map_errno_from_nt_status(status);
		TALLOC_FREE(result);
		return ret;
	}

	if (!result->fsp->fsp_flags.is_directory) {
		TALLOC_FREE(result);
		return EINVAL;
	}

	*_result = result;
	return 0;
}

static int streams_depot_rootdir_pathref(TALLOC_CTX *mem_ctx,
					 vfs_handle_struct *handle,
					 NTTIME twrp,
					 uint32_t base_name_flags,
					 bool create_it,
					 struct smb_filename **_result)
{
	struct streams_depot_config_data *config = NULL;
	struct connection_struct *conn = handle->conn;
	struct smb_filename *result = NULL;
	struct smb_filename *parent = NULL;
	struct smb_filename *rel = NULL;
	NTSTATUS status;
	int ret = EINVAL;

	SMB_VFS_HANDLE_GET_DATA(handle,
				config,
				struct streams_depot_config_data,
				return EINVAL);

	result = synthetic_smb_fname(
		mem_ctx, config->directory, NULL, NULL, twrp, base_name_flags);
	if (result == NULL) {
		return ENOMEM;
	}

	status = openat_pathref_fsp(conn->cwd_fsp, result);
	if (NT_STATUS_IS_OK(status)) {
		*_result = result;
		return 0;
	}

	if (!NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND)) {
		goto fail_ntstatus;
	}
	if (!create_it) {
		goto fail_ntstatus;
	}

	status = SMB_VFS_PARENT_PATHNAME(conn, mem_ctx, result, &parent, &rel);
	TALLOC_FREE(result);
	if (!NT_STATUS_IS_OK(status)) {
		goto fail_ntstatus;
	}

	status = openat_pathref_fsp(conn->cwd_fsp, parent);
	if (!NT_STATUS_IS_OK(status)) {
		goto fail_ntstatus;
	}

	ret = streams_depot_mkdir_pathref(
		mem_ctx, handle, parent->fsp, rel->base_name, 0755, &result);
	if (ret != 0) {
		goto fail;
	}

	*_result = result;
	result = NULL;

fail_ntstatus:
	ret = map_errno_from_nt_status(status);
fail:
	TALLOC_FREE(rel);
	TALLOC_FREE(parent);
	TALLOC_FREE(result);
	return ret;
}

struct streams_depot_dirnames {
	char first[3];
	char second[3];
	char id_hex[33];
	char full[39]; /* first+second+id_hex + slashes */
};

static void streams_depot_get_dirnames(connection_struct *conn,
				       const struct stat_ex *sbuf,
				       struct streams_depot_dirnames *names)
{
	struct file_id id = SMB_VFS_FILE_ID_CREATE(conn, sbuf);
	uint8_t first, second;
	uint8_t id_buf[16];
	uint32_t hash;

	push_file_id_16(id_buf, &id);

	hash = hash_fn(data_blob_const(id_buf, sizeof(id_buf)));
	first = hash & 0xff;
	second = (hash >> 8) & 0xff;

	snprintf(names->first, sizeof(names->first), "%2.2X", first);
	snprintf(names->second, sizeof(names->second), "%2.2X", second);
	hex_encode_buf(names->id_hex, id_buf, sizeof(id_buf));

	snprintf(names->full,
		 sizeof(names->full),
		 "%s/%s/%s",
		 names->first,
		 names->second,
		 names->id_hex);
}

static bool stream_dir_valid(struct connection_struct *conn,
			     struct files_struct *base_fsp)
{
	int ret;
	char buf;

	ret = SMB_VFS_FGETXATTR(base_fsp,
				SAMBA_XATTR_MARKER,
				&buf,
				sizeof(buf));
	if (ret != sizeof(buf)) {
		DBG_DEBUG("FGETXATTR failed: %s\n", strerror(errno));
		return false;
	}
	if (buf != '1') {
		DBG_DEBUG("Got wrong buffer content: '%c'\n", buf);
		return false;
	}

	return true;
}

static int stream_dir_parent_pathref(
	TALLOC_CTX *mem_ctx,
	NTTIME twrp,
	uint32_t base_fname_flags,
	struct files_struct *rootdir_fsp,
	const struct streams_depot_dirnames *dirnames,
	struct smb_filename **_result)
{
	char stream_dir_parent[6];
	struct smb_filename *result = NULL;
	NTSTATUS status;

	snprintf(stream_dir_parent,
		 sizeof(stream_dir_parent),
		 "%s/%s",
		 dirnames->first,
		 dirnames->second);

	result = synthetic_smb_fname(mem_ctx,
				     stream_dir_parent,
				     NULL,
				     NULL,
				     twrp,
				     base_fname_flags | UCF_POSIX_PATHNAMES);
	if (result == NULL) {
		return ENOMEM;
	}

	status = openat_pathref_fsp(rootdir_fsp, result);
	if (!NT_STATUS_IS_OK(status)) {
		TALLOC_FREE(result);
		return map_errno_from_nt_status(status);
	}

	*_result = result;
	return 0;
}

static int stream_dir_remove_invalid(
	vfs_handle_struct *handle,
	NTTIME twrp,
	uint32_t base_fname_flags,
	struct files_struct *rootdir_fsp,
	struct files_struct *stream_dirfsp,
	const struct streams_depot_dirnames *dirnames)
{
	struct streams_depot_config_data *config = NULL;
	NTSTATUS status;
	struct smb_filename *stream_dir_parent_fname = NULL;
	struct smb_filename *stream_dir_old = NULL;
	struct smb_filename *stream_dir_new = NULL;
	struct vfs_rename_how rhow = {};
	int ret;

	SMB_VFS_HANDLE_GET_DATA(handle,
				config,
				struct streams_depot_config_data,
				return EINVAL);
	if (config->delete_lost) {
		DBG_NOTICE("Someone has recreated a file under an "
			   "existing inode. Removing: %s\n",
			   fsp_str_dbg(stream_dirfsp));
		status = recursive_rmdir_fsp(stream_dirfsp);
		return map_errno_from_nt_status(status);
	}

	ret = stream_dir_parent_pathref(talloc_tos(),
					twrp,
					base_fname_flags,
					rootdir_fsp,
					dirnames,
					&stream_dir_parent_fname);
	if (ret != 0) {
		return ret;
	}

	stream_dir_old = synthetic_smb_fname(stream_dir_parent_fname,
					     dirnames->id_hex,
					     NULL,
					     NULL,
					     twrp,
					     base_fname_flags |
						     UCF_POSIX_PATHNAMES);
	if (stream_dir_old == NULL) {
		TALLOC_FREE(stream_dir_parent_fname);
		return ENOMEM;
	}

	do {
		char randstr[33];
		char loststr[sizeof(randstr) + 5];

		generate_random_str_list_buf(randstr,
					     sizeof(randstr),
					     hexchars_lower);
		snprintf(loststr, sizeof(loststr), "lost-%s", randstr);

		stream_dir_new = synthetic_smb_fname(
			stream_dir_parent_fname,
			loststr,
			NULL,
			NULL,
			twrp,
			base_fname_flags | UCF_POSIX_PATHNAMES);
		if (stream_dir_new == NULL) {
			TALLOC_FREE(stream_dir_parent_fname);
			return ENOMEM;
		}

		ret = SMB_VFS_NEXT_RENAMEAT(handle,
					    stream_dir_parent_fname->fsp,
					    stream_dir_old,
					    stream_dir_parent_fname->fsp,
					    stream_dir_new,
					    &rhow);
	} while ((ret == -1) && (errno == EEXIST));

	if (ret == -1) {
		ret = errno;
		TALLOC_FREE(stream_dir_parent_fname);
		return ret;
	}

	TALLOC_FREE(stream_dir_parent_fname);
	return 0;
}

static int stream_dir_pathref(TALLOC_CTX *mem_ctx,
			      vfs_handle_struct *handle,
			      const struct smb_filename *base,
			      const struct stat_ex *base_sbuf,
			      bool create_it,
			      struct smb_filename **_result)
{
	struct streams_depot_config_data *config = NULL;
	struct connection_struct *conn = handle->conn;
	struct smb_filename *rootdir_pathref = NULL;
	struct smb_filename *tmp_first = NULL;
	struct smb_filename *tmp_second = NULL;
	struct smb_filename *result = NULL;
	struct streams_depot_dirnames dirnames;
	NTSTATUS status;
	int ret = EINVAL;

	SMB_VFS_HANDLE_GET_DATA(handle,
				config,
				struct streams_depot_config_data,
				return EINVAL);

	ret = streams_depot_rootdir_pathref(talloc_tos(),
					    handle,
					    base->twrp,
					    base->flags,
					    create_it,
					    &rootdir_pathref);
	if (ret != 0) {
		return ret;
	}

	streams_depot_get_dirnames(conn, base_sbuf, &dirnames);

	result = synthetic_smb_fname(mem_ctx,
				     dirnames.full,
				     NULL,
				     NULL,
				     base->twrp,
				     base->flags | UCF_POSIX_PATHNAMES);
	if (result == NULL) {
		ret = ENOMEM;
		goto fail;
	}

	status = openat_pathref_fsp(rootdir_pathref->fsp, result);
	if (NT_STATUS_IS_OK(status)) {
		if (config->check_valid && !stream_dir_valid(conn, base->fsp))
		{
			ret = stream_dir_remove_invalid(handle,
							base->twrp,
							base->flags,
							rootdir_pathref->fsp,
							result->fsp,
							&dirnames);
			if (ret != 0) {
				TALLOC_FREE(rootdir_pathref);
				TALLOC_FREE(result);
				return ret;
			}
		}

		TALLOC_FREE(rootdir_pathref);
		*_result = result;
		return 0;
	}
	if (!NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_PATH_NOT_FOUND) &&
	    !NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND))
	{
		ret = map_errno_from_nt_status(status);
		goto fail;
	}
	if (!create_it) {
		ret = ENOENT;
		goto fail;
	}

	ret = streams_depot_mkdir_pathref(talloc_tos(),
					  handle,
					  rootdir_pathref->fsp,
					  dirnames.first,
					  0755,
					  &tmp_first);
	TALLOC_FREE(rootdir_pathref);
	if (ret != 0) {
		goto fail;
	}

	ret = streams_depot_mkdir_pathref(talloc_tos(),
					  handle,
					  tmp_first->fsp,
					  dirnames.second,
					  0755,
					  &tmp_second);
	TALLOC_FREE(tmp_first);
	if (ret != 0) {
		goto fail;
	}

	ret = streams_depot_mkdir_pathref(mem_ctx,
					  handle,
					  tmp_second->fsp,
					  dirnames.id_hex,
					  0755,
					  &result);
	TALLOC_FREE(tmp_second);
	if (ret != 0) {
		goto fail;
	}

	*_result = result;
	return 0;

fail:
	TALLOC_FREE(tmp_first);
	TALLOC_FREE(tmp_second);
	TALLOC_FREE(rootdir_pathref);
	TALLOC_FREE(result);
	return ret;
}

static int stream_name(TALLOC_CTX *mem_ctx,
		       const char *stream_name_in,
		       char **_result)
{
	const char *stype = strchr_m(stream_name_in + 1, ':');
	char *result = NULL;

	if ((stype != NULL) && !strequal(stype, ":$DATA")) {
		return EINVAL;
	}

	if (stype == NULL) {
		stype = stream_name_in + strlen(stream_name_in);
	}

	result = talloc_asprintf(mem_ctx,
				 "%.*s:$DATA",
				 (int)(stype - stream_name_in),
				 stream_name_in);
	if (result == NULL) {
		return ENOMEM;
	}

	*_result = result;
	return 0;
}

static NTSTATUS walk_streams(vfs_handle_struct *handle,
			     struct files_struct *fsp,
			     bool (*fn)(struct smb_filename *stream_dir_fname,
					const char *dirent,
					void *private_data),
			     void *private_data)
{
	struct smb_filename *stream_dir = NULL;
	struct smb_Dir *dir_hnd = NULL;
	const char *dname = NULL;
	char *talloced = NULL;
	NTSTATUS status;
	int ret;

	ret = stream_dir_pathref(talloc_tos(),
				 handle,
				 fsp->fsp_name,
				 &fsp->fsp_name->st,
				 false,
				 &stream_dir);
	if (ret == ENOENT) {
		return NT_STATUS_OK;
	}
	if (ret != 0) {
		return map_nt_error_from_unix(ret);
	}

	status = OpenDir_from_pathref(
		talloc_tos(), stream_dir->fsp, NULL, 0, &dir_hnd);
	if (!NT_STATUS_IS_OK(status)) {
		TALLOC_FREE(stream_dir);
		return status;
	}

	while ((dname = ReadDirName(dir_hnd, &talloced)) != NULL) {
		if (ISDOT(dname) || ISDOTDOT(dname)) {
			TALLOC_FREE(talloced);
			continue;
		}

		DBG_DEBUG("dirent=%s\n", dname);

		if (!fn(stream_dir, dname, private_data)) {
			TALLOC_FREE(talloced);
			break;
		}
		TALLOC_FREE(talloced);
	}

	TALLOC_FREE(dir_hnd);
	TALLOC_FREE(stream_dir);

	return NT_STATUS_OK;
}

static int streams_depot_fstatat(struct vfs_handle_struct *handle,
				 const struct files_struct *dirfsp,
				 const struct smb_filename *smb_fname,
				 SMB_STRUCT_STAT *sbuf,
				 int flags)
{
	struct connection_struct *conn = handle->conn;
	struct files_struct *local_dirfsp = NULL;
	struct smb_filename *base_fname = NULL;
	struct smb_filename *base_fname_rel = NULL;
	struct smb_filename *stream_dir = NULL;
	struct smb_filename sname = {
		.twrp = smb_fname->twrp,
		.flags = smb_fname->flags,
	};
	NTSTATUS status;
	int ret = -1;

	DBG_DEBUG("called for [%s/%s]\n",
		  dirfsp->fsp_name->base_name,
		  smb_fname_str_dbg(smb_fname));

	if (!is_named_stream(smb_fname)) {
		return SMB_VFS_NEXT_FSTATAT(
			handle, dirfsp, smb_fname, sbuf, flags);
	}

	status = filename_convert_dirfsp_rel(
		talloc_tos(),
		conn,
		discard_const_p(struct files_struct, dirfsp),
		smb_fname->base_name,
		(flags & AT_SYMLINK_NOFOLLOW) ? UCF_LCOMP_LNK_OK : 0,
		smb_fname->twrp,
		&local_dirfsp,
		&base_fname,
		&base_fname_rel);

	TALLOC_FREE(base_fname_rel);

	if (!NT_STATUS_IS_OK(status)) {
		errno = map_errno_from_nt_status(status);
		return -1;
	}

	close_file_free(NULL, &local_dirfsp, ERROR_CLOSE);

	if (!VALID_STAT(base_fname->st)) {
		TALLOC_FREE(base_fname);
		errno = ENOENT;
		return -1;
	}

	ret = stream_dir_pathref(talloc_tos(),
				 handle,
				 base_fname,
				 &base_fname->st,
				 false,
				 &stream_dir);
	if (ret != 0) {
		TALLOC_FREE(base_fname);
		errno = ret;
		return -1;
	}

	ret = stream_name(talloc_tos(),
			  smb_fname->stream_name,
			  &sname.base_name);
	if (ret != 0) {
		TALLOC_FREE(base_fname);
		errno = ret;
		return -1;
	}

	ret = SMB_VFS_NEXT_FSTATAT(handle, stream_dir->fsp, &sname, sbuf, 0);

	{
		int err = errno;
		TALLOC_FREE(sname.base_name);
		TALLOC_FREE(base_fname);
		errno = err;
	}

	return ret;
}

static int streams_depot_stat(vfs_handle_struct *handle,
			      struct smb_filename *smb_fname)
{
	return streams_depot_fstatat(
		handle, handle->conn->cwd_fsp, smb_fname, &smb_fname->st, 0);
}

static int streams_depot_lstat(vfs_handle_struct *handle,
			       struct smb_filename *smb_fname)
{
	return streams_depot_fstatat(handle,
				     handle->conn->cwd_fsp,
				     smb_fname,
				     &smb_fname->st,
				     AT_SYMLINK_NOFOLLOW);
}

static int streams_depot_openat(struct vfs_handle_struct *handle,
				const struct files_struct *dirfsp,
				const struct smb_filename *smb_fname,
				struct files_struct *fsp,
				const struct vfs_open_how *how)
{
	struct streams_depot_config_data *config = NULL;
	struct smb_filename *base_name = NULL;
	struct smb_filename *stream_dir = NULL;
	char *sname = NULL;
	struct smb_filename *smb_fname_stream = NULL;
	bool create_it;
	int ret = -1;

	SMB_VFS_HANDLE_GET_DATA(handle,
				config,
				struct streams_depot_config_data,
				return EINVAL);

	if (!is_named_stream(smb_fname)) {
		return SMB_VFS_NEXT_OPENAT(
			handle, dirfsp, smb_fname, fsp, how);
	}

	if ((how->resolve & ~(VFS_OPEN_HOW_WITH_BACKUP_INTENT |
			      VFS_OPEN_HOW_RESOLVE_NO_XDEV)) != 0)
	{
		errno = ENOSYS;
		return -1;
	}

	SMB_ASSERT(fsp_is_alternate_stream(fsp));
	SMB_ASSERT(dirfsp == NULL);

	base_name = fsp->base_fsp->fsp_name;
	SMB_ASSERT(VALID_STAT(base_name->st));

	create_it = (how->flags & O_CREAT);

	ret = stream_name(talloc_tos(), fsp->fsp_name->stream_name, &sname);
	if (ret != 0) {
		errno = ret;
		ret = -1;
		goto done;
	}

	ret = stream_dir_pathref(talloc_tos(),
				 handle,
				 base_name,
				 &base_name->st,
				 create_it,
				 &stream_dir);
	if (ret != 0) {
		errno = ret;
		ret = -1;
		goto done;
	}

	smb_fname_stream = synthetic_smb_fname(talloc_tos(),
					       sname,
					       NULL,
					       NULL,
					       fsp->fsp_name->twrp,
					       fsp->fsp_name->flags);
	if (smb_fname_stream == NULL) {
		errno = ENOMEM;
		ret = -1;
		goto done;
	}

	if (create_it && config->check_valid) {
		char buf = '1';

		DBG_DEBUG("marking file %s as valid\n",
			  fsp->base_fsp->fsp_name->base_name);

		ret = SMB_VFS_FSETXATTR(fsp->base_fsp,
					SAMBA_XATTR_MARKER,
					&buf,
					sizeof(buf),
					0);

		if (ret == -1) {
			DBG_DEBUG("FSETXATTR failed: %s\n", strerror(errno));
			goto done;
		}
	}

	ret = SMB_VFS_NEXT_OPENAT(
		handle, stream_dir->fsp, smb_fname_stream, fsp, how);

 done:
	{
		int err = errno;
		TALLOC_FREE(stream_dir);
		TALLOC_FREE(smb_fname_stream);
		TALLOC_FREE(sname);
		errno = err;
	}
	 return ret;
}

static int streams_depot_unlinkat_stream(vfs_handle_struct *handle,
					 struct files_struct *dirfsp,
					 const struct smb_filename *smb_fname)
{
	struct smb_filename sname = {};
	struct smb_filename *base_name = NULL;
	struct smb_filename *stream_dir = NULL;
	NTSTATUS status;
	int ret = -1;

	if (smb_fname->fsp != NULL) {
		base_name = smb_fname->fsp->base_fsp->fsp_name;
	} else {
		base_name = cp_smb_filename_nostream(talloc_tos(), smb_fname);
		if (base_name == NULL) {
			errno = ENOMEM;
			goto done;
		}

		status = openat_pathref_fsp(dirfsp, base_name);
		if (!NT_STATUS_IS_OK(status)) {
			errno = map_errno_from_nt_status(status);
			goto done;
		}
	}

	ret = stream_dir_pathref(talloc_tos(),
				 handle,
				 base_name,
				 &base_name->st,
				 false,
				 &stream_dir);
	if (ret != 0) {
		errno = ret;
		ret = -1;
		goto done;
	}

	sname = (struct smb_filename){
		.flags = smb_fname->flags,
		.twrp = smb_fname->twrp,
	};

	ret = stream_name(stream_dir,
			  smb_fname->stream_name,
			  &sname.base_name);
	if (ret != 0) {
		errno = ret;
		ret = -1;
		goto done;
	}

	ret = SMB_VFS_NEXT_UNLINKAT(handle, stream_dir->fsp, &sname, 0);
done:
	{
		int err = errno;
		TALLOC_FREE(stream_dir);
		errno = err;
	}
	return ret;
}

static int streams_depot_unlinkat(vfs_handle_struct *handle,
				  struct files_struct *dirfsp,
				  const struct smb_filename *smb_fname,
				  int flags)
{
	struct connection_struct *conn = handle->conn;
	struct streams_depot_dirnames dirnames;
	struct smb_filename *rootdir_pathref = NULL;
	struct smb_filename *stream_dir_parent = NULL;
	struct smb_filename sname = {};
	struct stat_ex st;
	int ret;

	if (is_named_stream(smb_fname)) {
		ret = streams_depot_unlinkat_stream(handle, dirfsp, smb_fname);
		return ret;
	}

	ret = SMB_VFS_FSTATAT(
		conn, dirfsp, smb_fname, &st, AT_SYMLINK_NOFOLLOW);
	if (ret != 0) {
		return ret;
	}
	streams_depot_get_dirnames(conn, &smb_fname->st, &dirnames);

	ret = streams_depot_rootdir_pathref(talloc_tos(),
					    handle,
					    smb_fname->twrp,
					    smb_fname->flags,
					    false,
					    &rootdir_pathref);
	if (ret == 0) {
		ret = stream_dir_parent_pathref(talloc_tos(),
						smb_fname->twrp,
						smb_fname->flags,
						rootdir_pathref->fsp,
						&dirnames,
						&stream_dir_parent);
		TALLOC_FREE(rootdir_pathref);
	}

	if (ret == 0) {
		sname = (struct smb_filename){
			.base_name = dirnames.id_hex,
			.twrp = smb_fname->twrp,
			.flags = smb_fname->flags,
		};

		ret = SMB_VFS_UNLINKAT(conn,
				       stream_dir_parent->fsp,
				       &sname,
				       AT_REMOVEDIR);
		if (ret == -1) {
			DBG_DEBUG("Removing %s failed: %s, ignoring\n",
				  dirnames.id_hex,
				  strerror(errno));
		}
	}

	TALLOC_FREE(stream_dir_parent);

	ret = SMB_VFS_NEXT_UNLINKAT(handle, dirfsp, smb_fname, flags);
	return ret;
}

static int streams_depot_rename_stream(struct vfs_handle_struct *handle,
				       struct files_struct *src_fsp,
				       const char *dst_name,
				       bool replace_if_exists)
{
	struct smb_filename *basename = src_fsp->base_fsp->fsp_name;
	struct smb_filename *stream_dir = NULL;
	struct smb_filename smb_fname_src_stream = {};
	struct smb_filename smb_fname_dst_stream = {};
	struct vfs_rename_how how = {.flags = 0};
	int ret;

	ret = stream_dir_pathref(talloc_tos(),
				 handle,
				 basename,
				 &basename->st,
				 false,
				 &stream_dir);
	if (ret != 0) {
		errno = ret;
		ret = -1;
		goto done;
	}

	ret = stream_name(talloc_tos(),
			  src_fsp->fsp_name->stream_name,
			  &smb_fname_src_stream.base_name);
	if (ret != 0) {
		errno = ret;
		ret = -1;
		goto done;
	}

	ret = stream_name(talloc_tos(),
			  dst_name,
			  &smb_fname_dst_stream.base_name);
	if (ret != 0) {
		errno = ret;
		ret = -1;
		goto done;
	}

	if (!replace_if_exists) {
		struct stat_ex st;

		ret = SMB_VFS_FSTATAT(handle->conn,
				      stream_dir->fsp,
				      &smb_fname_dst_stream,
				      &st,
				      AT_SYMLINK_NOFOLLOW);
		if (ret == 0) {
			errno = EEXIST;
			ret = -1;
			goto done;
		}
	}

	ret = SMB_VFS_NEXT_RENAMEAT(handle,
				    stream_dir->fsp,
				    &smb_fname_src_stream,
				    stream_dir->fsp,
				    &smb_fname_dst_stream,
				    &how);
done:
	{
		int err = errno;
		TALLOC_FREE(stream_dir);
		TALLOC_FREE(smb_fname_src_stream.base_name);
		TALLOC_FREE(smb_fname_dst_stream.base_name);
		errno = err;
	}
	return ret;
}

static bool add_one_stream(TALLOC_CTX *mem_ctx, unsigned int *num_streams,
			   struct stream_struct **streams,
			   const char *name, off_t size,
			   off_t alloc_size)
{
	struct stream_struct *tmp;

	tmp = talloc_realloc(mem_ctx, *streams, struct stream_struct,
				   (*num_streams)+1);
	if (tmp == NULL) {
		return false;
	}

	tmp[*num_streams].name = talloc_strdup(tmp, name);
	if (tmp[*num_streams].name == NULL) {
		return false;
	}

	tmp[*num_streams].size = size;
	tmp[*num_streams].alloc_size = alloc_size;

	*streams = tmp;
	*num_streams += 1;
	return true;
}

struct streaminfo_state {
	TALLOC_CTX *mem_ctx;
	vfs_handle_struct *handle;
	unsigned int num_streams;
	struct stream_struct *streams;
	NTSTATUS status;
};

static bool collect_one_stream(struct smb_filename *stream_dir_fname,
			       const char *dirent,
			       void *private_data)
{
	struct streaminfo_state *state =
		(struct streaminfo_state *)private_data;
	vfs_handle_struct *handle = state->handle;
	connection_struct *conn = handle->conn;
	struct smb_filename dirent_fname = {
		.base_name = discard_const_p(char, dirent),
		.flags = stream_dir_fname->flags,
		.twrp = stream_dir_fname->twrp,
	};
	struct stat_ex st;
	int ret;
	bool ok;

	ret = SMB_VFS_NEXT_FSTATAT(conn,
				   stream_dir_fname->fsp,
				   &dirent_fname,
				   &st,
				   AT_SYMLINK_NOFOLLOW);
	if (ret != 0) {
		/* ignore */
		return true;
	}

	ok = add_one_stream(state->mem_ctx,
			    &state->num_streams,
			    &state->streams,
			    dirent,
			    st.st_ex_size,
			    SMB_VFS_GET_ALLOC_SIZE(conn, NULL, &st));
	if (!ok) {
		state->status = NT_STATUS_NO_MEMORY;
		return false;
	}

	return true;
}

static NTSTATUS streams_depot_fstreaminfo(vfs_handle_struct *handle,
					 struct files_struct *fsp,
					 TALLOC_CTX *mem_ctx,
					 unsigned int *pnum_streams,
					 struct stream_struct **pstreams)
{
	struct streaminfo_state state = {
		.streams = *pstreams,
		.num_streams = *pnum_streams,
		.mem_ctx = mem_ctx,
		.handle = handle,
		.status = NT_STATUS_OK,
	};
	NTSTATUS status;

	status = walk_streams(handle, fsp, collect_one_stream, &state);

	if (!NT_STATUS_IS_OK(status)) {
		TALLOC_FREE(state.streams);
		return status;
	}

	if (!NT_STATUS_IS_OK(state.status)) {
		TALLOC_FREE(state.streams);
		return state.status;
	}

	*pnum_streams = state.num_streams;
	*pstreams = state.streams;

	status = SMB_VFS_NEXT_FSTREAMINFO(
		handle, metadata_fsp(fsp), mem_ctx, pnum_streams, pstreams);
	return status;
}

static uint32_t streams_depot_fs_capabilities(struct vfs_handle_struct *handle,
			enum timestamp_set_resolution *p_ts_res)
{
	return SMB_VFS_NEXT_FS_CAPABILITIES(handle, p_ts_res) | FILE_NAMED_STREAMS;
}

static int streams_depot_connect(struct vfs_handle_struct *handle,
				 const char *service,
				 const char *user)
{
	struct connection_struct *conn = handle->conn;
	struct streams_depot_config_data *config = NULL;
	int ret;

	config = talloc_zero(conn, struct streams_depot_config_data);
	if (config == NULL) {
		goto nomem;
	}

	ret = SMB_VFS_NEXT_CONNECT(handle, service, user);
	if (ret < 0) {
		TALLOC_FREE(config);
		return ret;
	}

	config->check_valid = lp_parm_bool(SNUM(conn),
					   "streams_depot",
					   "check_valid",
					   true);

	config->directory = stream_rootdir(handle, config);
	if (config->directory == NULL) {
		goto nomem;
	}

	config->delete_lost = lp_parm_bool(SNUM(conn),
					   "streams_depot",
					   "delete_lost",
					   false);

	SMB_VFS_HANDLE_SET_DATA(handle,
				config,
				NULL,
				struct streams_depot_config_data,
				return -1);

	return 0;
nomem:
	TALLOC_FREE(config);
	errno = ENOMEM;
	return -1;
}

static struct vfs_fn_pointers vfs_streams_depot_fns = {
	.connect_fn = streams_depot_connect,
	.fs_capabilities_fn = streams_depot_fs_capabilities,
	.openat_fn = streams_depot_openat,
	.stat_fn = streams_depot_stat,
	.lstat_fn = streams_depot_lstat,
	.fstatat_fn = streams_depot_fstatat,
	.unlinkat_fn = streams_depot_unlinkat,
	.rename_stream_fn = streams_depot_rename_stream,
	.fstreaminfo_fn = streams_depot_fstreaminfo,
};

static_decl_vfs;
NTSTATUS vfs_streams_depot_init(TALLOC_CTX *ctx)
{
	return smb_register_vfs(SMB_VFS_INTERFACE_VERSION, "streams_depot",
				&vfs_streams_depot_fns);
}
