/*
 * Copyright (c) [2011-2015] Novell, Inc.
 * Copyright (c) [2016-2023] SUSE LLC
 *
 * All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of version 2 of the GNU General Public License as published
 * by the Free Software Foundation.
 *
 * 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, contact Novell, Inc.
 *
 * To contact Novell about this file by physical or electronic mail, you may
 * find current contact information at www.novell.com.
 */


#include "config.h"

#include <cstring>
#include <sys/stat.h>
#include <sys/types.h>
#include <cerrno>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <asm/types.h>
#ifdef HAVE_LIBBTRFS
#ifdef HAVE_BTRFS_VERSION_H
#include <btrfs/version.h>
#else
#define BTRFS_LIB_VERSION (100)
#endif
#include <btrfs/ioctl.h>
#include <btrfs/send.h>
#include <btrfs/send-stream.h>
#include <btrfs/send-utils.h>
#include <boost/version.hpp>
#include <boost/thread.hpp>
#endif
#include <regex>
#include <boost/algorithm/string.hpp>

#include "snapper/Log.h"
#include "snapper/Btrfs.h"
#include "snapper/BtrfsUtils.h"
#include "snapper/File.h"
#include "snapper/Hooks.h"
#include "snapper/Snapper.h"
#include "snapper/SnapperTmpl.h"
#include "snapper/SnapperDefines.h"
#include "snapper/Acls.h"
#include "snapper/Exception.h"
#ifdef ENABLE_ROLLBACK
#include "snapper/MntTable.h"
#endif
#ifdef ENABLE_SELINUX
#include "snapper/Selinux.h"
#endif


namespace snapper
{
    using namespace std;


    Filesystem*
    Btrfs::create(const string& fstype, const string& subvolume, const string& root_prefix)
    {
	if (fstype == "btrfs")
	    return new Btrfs(subvolume, root_prefix);

	return NULL;
    }


    Btrfs::Btrfs(const string& subvolume, const string& root_prefix)
	: Filesystem(subvolume, root_prefix), qgroup(no_qgroup)
    {
    }


    void
    Btrfs::evalConfigInfo(const ConfigInfo& config_info)
    {
#ifdef ENABLE_BTRFS_QUOTA

	string qgroup_str;
	if (config_info.get_value("QGROUP", qgroup_str) && !qgroup_str.empty())
	{
	    try
	    {
		qgroup = parse_qgroup(qgroup_str);
	    }
	    catch (const runtime_error& e)
	    {
		y2err("failed to parse qgroup '" << qgroup_str << "'");
		SN_THROW(InvalidConfigException());
	    }
	}
	else
	{
	    qgroup = no_qgroup;
	}

#endif
    }


    void
    Btrfs::createConfig() const
    {
	SDir subvolume_dir = openSubvolumeDir();

	try
	{
	    create_subvolume(subvolume_dir.fd(), ".snapshots");
	}
	catch (const runtime_error_with_errno& e)
	{
	    y2err("create subvolume failed, " << e.what());

	    switch (e.error_number)
	    {
		case EEXIST:
		    SN_THROW(CreateConfigFailedException("creating btrfs subvolume .snapshots failed "
							 "since it already exists"));
		    break;

		default:
		    SN_THROW(CreateConfigFailedException("creating btrfs subvolume .snapshots failed"));
	    }
	}

	SFile x(subvolume_dir, ".snapshots");
#ifdef ENABLE_SELINUX
	try
	{
	    SnapperContexts scontexts;

	    x.fsetfilecon(scontexts.subvolume_context());
	}
	catch (const SelinuxException& e)
	{
	    SN_CAUGHT(e);
	    // fall through intentional
	}
#endif
	struct stat stat;
	if (x.stat(&stat, 0) == 0)
	    x.chmod(stat.st_mode & ~0027, 0);
    }


    void
    Btrfs::deleteConfig() const
    {
	SDir subvolume_dir = openSubvolumeDir();

#ifdef ENABLE_ROLLBACK
	if (subvolume == "/")
	{
	    subvolume_dir.umount(".snapshots");

	    removeFromFstab();
	}
#endif

	try
	{
	    delete_subvolume(subvolume_dir.fd(), ".snapshots");
	}
	catch (const runtime_error& e)
	{
	    y2err("delete subvolume failed, " << e.what());
	    SN_THROW(DeleteConfigFailedException("deleting btrfs snapshot failed"));
	}
    }


    void
    Btrfs::addToFstab(const string& default_subvolume_name) const
    {
#ifdef ENABLE_ROLLBACK
	if (subvolume == "/")
	{
	    try
	    {
		addToFstabHelper(default_subvolume_name);
	    }
	    catch (const runtime_error& e)
	    {
		y2err("adding to fstab failed, " << e.what());
	    }
	}
#endif
    }


    void
    Btrfs::removeFromFstab() const
    {
#ifdef ENABLE_ROLLBACK
	if (subvolume == "/")
	{
	    try
	    {
		removeFromFstabHelper();
	    }
	    catch (const runtime_error& e)
	    {
		y2err("removing from fstab failed, " << e.what());
	    }
	}
#endif
    }


    string
    Btrfs::snapshotDir(unsigned int num) const
    {
	return (subvolume == "/" ? "" : subvolume) + "/.snapshots/" + decString(num) +
	    "/snapshot";
    }


    SDir
    Btrfs::openSubvolumeDir() const
    {
	SDir subvolume_dir = Filesystem::openSubvolumeDir();

	struct stat stat;
	if (subvolume_dir.stat(&stat) != 0)
	{
	    SN_THROW(IOErrorException("stat on subvolume directory failed"));
	}

	if (!is_subvolume(stat))
	{
	    SN_THROW(IOErrorException("subvolume is not a btrfs subvolume"));
	}

	return subvolume_dir;
    }


    SDir
    Btrfs::openInfosDir() const
    {
	SDir subvolume_dir = openSubvolumeDir();
	SDir infos_dir(subvolume_dir, ".snapshots");

	struct stat stat;
	if (infos_dir.stat(&stat) != 0)
	{
	    SN_THROW(IOErrorException("stat on info directory failed"));
	}

	if (!is_subvolume(stat))
	{
	    SN_THROW(IOErrorException(".snapshots is not a btrfs subvolume"));
	}

	if (stat.st_uid != 0)
	{
	    y2err(".snapshots must have owner root");
	    SN_THROW(IOErrorException(".snapshots must have owner root"));
	}

	if (stat.st_gid != 0 && stat.st_mode & S_IWGRP)
	{
	    y2err(".snapshots must have group root or must not be group-writable");
	    SN_THROW(IOErrorException(".snapshots must have group root or must not be group-writable"));
	}

	if (stat.st_mode & S_IWOTH)
	{
	    y2err(".snapshots must not be world-writable");
	    SN_THROW(IOErrorException(".snapshots must not be world-writable"));
	}

	return infos_dir;
    }


    SDir
    Btrfs::openSnapshotDir(unsigned int num) const
    {
	SDir info_dir = openInfoDir(num);
	SDir snapshot_dir(info_dir, "snapshot");

	return snapshot_dir;
    }


    SDir
    Btrfs::openGeneralDir() const
    {
	return openInfosDir();
    }


    void
    Btrfs::createSnapshot(unsigned int num, unsigned int num_parent, bool read_only, bool quota,
			  bool empty) const
    {
	if (num_parent == 0)
	{
	    SDir subvolume_dir = openSubvolumeDir();
	    SDir info_dir = openInfoDir(num);

	    try
	    {
		if (empty)
		    create_subvolume(info_dir.fd(), "snapshot");
		else
		    create_snapshot(subvolume_dir.fd(), info_dir.fd(), "snapshot", read_only,
				    quota ? qgroup : no_qgroup);
	    }
	    catch (const runtime_error& e)
	    {
		y2err("create snapshot failed, " << e.what());
		SN_THROW(CreateSnapshotFailedException());
	    }
	}
	else
	{
	    SDir snapshot_dir = openSnapshotDir(num_parent);
	    SDir info_dir = openInfoDir(num);

	    try
	    {
		create_snapshot(snapshot_dir.fd(), info_dir.fd(), "snapshot", read_only,
				quota ? qgroup : no_qgroup);
	    }
	    catch (const runtime_error& e)
	    {
		y2err("create snapshot failed, " << e.what());
		SN_THROW(CreateSnapshotFailedException());
	    }
	}
    }


#ifdef ENABLE_ROLLBACK

    void
    Btrfs::createSnapshotOfDefault(unsigned int num, bool read_only, bool quota) const
    {
	SDir subvolume_dir = openSubvolumeDir();
	subvolid_t id = get_default_id(subvolume_dir.fd());
	string name = get_subvolume(subvolume_dir.fd(), id);

	bool found = false;
	MtabData mtab_data;
	if (!getMtabData(subvolume, found, mtab_data))
	{
	    y2err("failed to find device");
	    SN_THROW(CreateSnapshotFailedException());
	}

	SDir infos_dir = openInfosDir();
	TmpMount tmp_mount(infos_dir, mtab_data.device, "tmp-mnt-XXXXXX", "btrfs", 0,
			   "subvol=" + name);

	SDir tmp_mount_dir(infos_dir, tmp_mount.getName());
	SDir info_dir = openInfoDir(num);

	try
	{
	    create_snapshot(tmp_mount_dir.fd(), info_dir.fd(), "snapshot", read_only,
			    quota ? qgroup : no_qgroup);
	}
	catch (const runtime_error& e)
	{
	    y2err("create snapshot failed, " << e.what());
	    SN_THROW(CreateSnapshotFailedException());
	}
    }

#else

    void
    Btrfs::createSnapshotOfDefault(unsigned int num, bool read_only, bool quota) const
    {
	throw std::logic_error("not implemented");
    }

#endif


    void
    Btrfs::deleteSnapshot(unsigned int num) const
    {
	SDir info_dir = openInfoDir(num);

	try
	{
#ifdef HAVE_LIBBTRFS
	    subvolid_t subvolid = get_id(openSnapshotDir(num).fd());
#endif

	    delete_subvolume(info_dir.fd(), "snapshot");

#ifdef HAVE_LIBBTRFS
	    deleted_subvolids.push_back(subvolid);
#endif

#ifdef ENABLE_BTRFS_QUOTA

	    // workaround for the kernel not deleting the qgroup of a
	    // subvolume when deleting the subvolume, see
	    // https://bugzilla.suse.com/show_bug.cgi?id=972511

	    try
	    {
		SDir general_dir = openGeneralDir();
		qgroup_destroy(general_dir.fd(), calc_qgroup(0, subvolid));
	    }
	    catch (const runtime_error& e)
	    {
		// Ignore that the qgroup could not be destroyed. Should not
		// cause problems except of having stale qgroups.
	    }

#endif
	}
	catch (const runtime_error& e)
	{
	    y2err("delete snapshot failed, " << e.what());
	    SN_THROW(DeleteSnapshotFailedException());
	}
    }


    bool
    Btrfs::isSnapshotMounted(unsigned int num) const
    {
	return true;
    }


    void
    Btrfs::mountSnapshot(unsigned int num) const
    {
    }


    void
    Btrfs::umountSnapshot(unsigned int num) const
    {
    }


    bool
    Btrfs::isSnapshotReadOnly(unsigned int num) const
    {
	SDir snapshot_dir = openSnapshotDir(num);
	return is_subvolume_read_only(snapshot_dir.fd());
    }


    void
    Btrfs::setSnapshotReadOnly(unsigned int num, bool read_only) const
    {
	SDir snapshot_dir = openSnapshotDir(num);
	set_subvolume_read_only(snapshot_dir.fd(), read_only);
    }


    bool
    Btrfs::checkSnapshot(unsigned int num) const
    {
	try
	{
	    SDir info_dir = openInfoDir(num);

	    struct stat stat;
	    int r = info_dir.stat("snapshot", &stat, AT_SYMLINK_NOFOLLOW);
	    return r == 0 && is_subvolume(stat);
	}
	catch (const IOErrorException& e)
	{
	    SN_CAUGHT(e);

	    // TODO the openInfoDir above logs an error although when this
	    // function is used from nextNumber the failure is ok

	    return false;
	}
    }


#ifdef HAVE_LIBBTRFS


    class StreamProcessor;


    class tree_node
    {
    public:

	typedef map<string, tree_node>::iterator iterator;
	typedef map<string, tree_node>::const_iterator const_iterator;

	tree_node() : status(0) {}

	int status;

	map<string, tree_node> childs;

	tree_node* find(const string& name);

	tree_node* insert(const string& name);

	bool erase(const string& name);

	bool rename(const string& o, const string& n);

	void dump(const string& prefix = "") const;

	unsigned int check(StreamProcessor* processor, const string& name, unsigned int status) const;
	void check(StreamProcessor* processor, const string& prefix = "");

	void result(cmpdirs_cb_t cb, const string& prefix = "") const;

    };


    tree_node*
    tree_node::find(const string& name)
    {
	string::size_type pos = name.find('/');
	if (pos == string::npos)
	{
	    iterator it = childs.find(name);
	    if (it == childs.end())
		return NULL;

	    return &it->second;
	}
	else
	{
	    string a = name.substr(0, pos);
	    iterator it = childs.find(a);
	    if (it == childs.end())
		return NULL;

	    string b = name.substr(pos + 1);
	    return it->second.find(b);
	}
    }


    tree_node*
    tree_node::insert(const string& name)
    {
	string::size_type pos = name.find('/');
	if (pos == string::npos)
	{
	    iterator it = childs.find(name);
	    if (it == childs.end())
		it = childs.insert(childs.end(), make_pair(name, tree_node()));

	    return &it->second;
	}
	else
	{
	    string a = name.substr(0, pos);
	    iterator it = childs.find(a);
	    if (it == childs.end())
		it = childs.insert(childs.end(), make_pair(a, tree_node()));

	    string b = name.substr(pos + 1);
	    return it->second.insert(b);
	}
    }


    bool
    tree_node::erase(const string& name)
    {
	string::size_type pos = name.find('/');
	if (pos == string::npos)
	{
	    iterator it = childs.find(name);
	    if (it == childs.end())
		return false;

	    if (it->second.childs.empty())
		childs.erase(it);
	    else
		it->second.status = 0;

	    return true;
	}
	else
	{
	    string a = name.substr(0, pos);
	    iterator it = childs.find(a);
	    if (it == childs.end())
		return false;

	    string b = name.substr(pos + 1);
	    it->second.erase(b);

	    if (it->second.status == 0 && it->second.childs.empty())
		childs.erase(it);

	    return true;
	}
    }


    bool
    tree_node::rename(const string& o, const string& n)
    {
	tree_node* oo = find(o);
	if (!oo)
	    return false;

	tree_node* nn = find(n);
	if (nn)
	    return false;

	nn = insert(n);
	swap(nn->childs, oo->childs);
	nn->status = oo->status;
	erase(o);

	return true;
    }


    void
    tree_node::dump(const string& prefix) const
    {
	for (const_iterator it = childs.begin(); it != childs.end(); ++it)
	{
	    if (prefix.empty())
	    {
		y2deb(it->first << "  " << statusToString(it->second.status));
		it->second.dump(it->first);
	    }
	    else
	    {
		y2deb(prefix + "/" + it->first << "  " << statusToString(it->second.status));
		it->second.dump(prefix + "/" + it->first);
	    }
	}
    }


    struct BtrfsSendReceiveException : public Exception
    {
	explicit BtrfsSendReceiveException() : Exception("btrfs send/receive error") {}
    };


    class StreamProcessor
    {
    public:

	StreamProcessor(const SDir& base, const SDir& dir1, const SDir& dir2);

	const SDir& base;
	const SDir& dir1;
	const SDir& dir2;

	void process(cmpdirs_cb_t cb);

	tree_node files;

	void created(const string& name);
	void deleted(const string& name);

    private:

	struct subvol_uuid_search sus;

	bool get_root_id(const string& path, u64* root_id);

	bool dumper(int fd);

	void do_send(u64 parent_root_id, const vector<u64>& clone_sources);

    };


    unsigned int
    tree_node::check(StreamProcessor* processor, const string& name, unsigned int status) const
    {
	if (status & CREATED) status = CREATED;
	if (status & DELETED) status = DELETED;

	if (status & (CONTENT | PERMISSIONS | OWNER | GROUP | XATTRS | ACL))
	{
	    // TODO check for content sometimes not required
	    status &= ~(CONTENT | PERMISSIONS | OWNER | GROUP | XATTRS | ACL);

	    string dirname = snapper::dirname(name);
	    string basename = snapper::basename(name);

	    SDir subdir1 = SDir::deepopen(processor->dir1, dirname);
	    SDir subdir2 = SDir::deepopen(processor->dir2, dirname);

	    status |= cmpFiles(SFile(subdir1, basename), SFile(subdir2, basename));
	}

	return status;
    }


    void
    tree_node::check(StreamProcessor* processor, const string& prefix)
    {
	for (iterator it = childs.begin(); it != childs.end(); ++it)
	{
	    if (prefix.empty())
	    {
		it->second.status = check(processor, it->first, it->second.status);
		it->second.check(processor, it->first);
	    }
	    else
	    {
		it->second.status = check(processor, prefix + "/" + it->first, it->second.status);
		it->second.check(processor, prefix + "/" + it->first);
	    }
	}
    }


    void
    tree_node::result(cmpdirs_cb_t cb, const string& prefix) const
    {
	for (const_iterator it = childs.begin(); it != childs.end(); ++it)
	{
	    if (prefix.empty())
	    {
		if (it->second.status != 0)
		    (cb)("/" + it->first, it->second.status);
		it->second.result(cb, it->first);
	    }
	    else
	    {
		if (it->second.status != 0)
		    (cb)("/" + prefix + "/" + it->first, it->second.status);
		it->second.result(cb, prefix + "/" + it->first);
	    }
	}
    }


    StreamProcessor::StreamProcessor(const SDir& base, const SDir& dir1, const SDir& dir2)
	: base(base), dir1(dir1), dir2(dir2)
    {
	memset(&sus, 0, sizeof(sus));
	int r = subvol_uuid_search_init(base.fd(), &sus);
	if (r < 0)
	{
	    y2err("failed to initialize subvol search (" << stringerror(r) << ")");
	    SN_THROW(BtrfsSendReceiveException());
	}
    }


    bool
    StreamProcessor::get_root_id(const string& path, u64* root_id)
    {
	struct subvol_info* si;
	si = subvol_uuid_search(&sus, 0, NULL, 0, path.c_str(), subvol_search_by_path);
	if (!si)
	    return false;

	*root_id = si->root_id;
	return true;
    }


    void
    StreamProcessor::created(const string& name)
    {
	tree_node* node = files.find(name);
	if (!node)
	{
	    node = files.insert(name);
	    node->status = CREATED;
	}
	else
	{
	    node->status &= ~(CREATED | DELETED);
	    node->status |= CONTENT | PERMISSIONS | OWNER | GROUP | XATTRS | ACL;
	}
    }


    void
    StreamProcessor::deleted(const string& name)
    {
	tree_node* node = files.find(name);
	if (!node)
	{
	    node = files.insert(name);
	    node->status = DELETED;
	}
	else
	{
	    files.erase(name);
	}
    }


    int
    process_subvol(const char* path, const u8* uuid, u64 ctransid, void* user)
    {
	return 0;
    }


    int
    process_snapshot(const char* path, const u8* uuid, u64 ctransid, const u8* parent_uuid,
		     u64 parent_ctransid, void* user)
    {
	return 0;
    }


    int
    process_mkfile(const char* path, void* user)
    {
	StreamProcessor* processor = (StreamProcessor*) user;

#ifdef DEBUG_PROCESS
	y2deb("mkfile path:'" << path << "'");
#endif

	processor->created(path);

	return 0;
    }


    int
    process_mkdir(const char* path, void* user)
    {
	StreamProcessor* processor = (StreamProcessor*) user;

#ifdef DEBUG_PROCESS
	y2deb("mkdir path:'" << path << "'");
#endif

	processor->created(path);

	return 0;
    }


    int
    process_mknod(const char* path, u64 mode, u64 dev, void* user)
    {
#ifdef DEBUG_PROCESS
	y2deb("mknod path:'" << path << "'");
#endif

	return 0;
    }


    int
    process_mkfifo(const char* path, void* user)
    {
#ifdef DEBUG_PROCESS
	y2deb("mkfifo path:'" << path << "'");
#endif

	return 0;
    }


    int
    process_mksock(const char* path, void* user)
    {
#ifdef DEBUG_PROCESS
	y2deb("mksock path:'" << path << "'");
#endif

	return 0;
    }


    int
    process_symlink(const char* path, const char* lnk, void* user)
    {
	StreamProcessor* processor = (StreamProcessor*) user;

#ifdef DEBUG_PROCESS
	y2deb("symlink path:'" << path << "' lnk:'" << lnk << "'");
#endif

	processor->created(path);

	return 0;
    }


    void
    merge(StreamProcessor* processor, tree_node* tmp, const string& from, const string& to,
	  const string& prefix = "")
    {
	for (tree_node::iterator it = tmp->childs.begin(); it != tmp->childs.end(); ++it)
	{
	    if (prefix.empty())
	    {
		string x = to + "/" + it->first;

		tree_node* node = processor->files.find(x);
		if (!node)
		{
		    node = processor->files.insert(x);
		    node->status = it->second.status;
		}
		else
		{
		    node->status &= ~(CREATED | DELETED);
		    node->status |= CONTENT | PERMISSIONS | OWNER | GROUP | XATTRS | ACL;
		}

		merge(processor, &it->second, from, to, x);
	    }
	    else
	    {
		string x = to + "/" + prefix + "/" + it->first;

		tree_node* node = processor->files.find(x);
		if (!node)
		{
		    node = processor->files.insert(x);
		    node->status = it->second.status;
		}
		else
		{
		    node->status &= ~(CREATED | DELETED);
		    node->status |= CONTENT | PERMISSIONS | OWNER | GROUP | XATTRS | ACL;
		}

		merge(processor, &it->second, from, to, x);
	    }
	}
    }


    int
    process_rename(const char* _from, const char* _to, void* user)
    {
	string from(_from);
	string to(_to);

	StreamProcessor* processor = (StreamProcessor*) user;

#ifdef DEBUG_PROCESS
	y2deb("rename from:'" << from << "' to:'" << to << "'");
#endif

	tree_node* it1 = processor->files.find(from);
	if (!it1)
	{
	    processor->deleted(from);
	    processor->created(to);

	    string dirname = snapper::dirname(from);
	    string basename = snapper::basename(from);

	    struct stat buf;
	    SDir tmpdir1 = SDir::deepopen(processor->dir1, dirname);
	    if (tmpdir1.stat(basename, &buf, AT_SYMLINK_NOFOLLOW) == 0 && S_ISDIR(buf.st_mode))
	    {
		SDir tmpdir2(tmpdir1, basename);

		vector<string> entries = tmpdir2.entries_recursive();
		for (vector<string>::const_iterator it = entries.begin(); it != entries.end(); ++it)
		{
		    processor->deleted(from + "/" + *it);
		    processor->created(to + "/" + *it);
		}
	    }
	}
	else
	{
	    tree_node* it2 = processor->files.find(to);
	    if (!it2)
	    {
		processor->files.rename(from, to);
	    }
	    else
	    {
		tree_node tmp;
		swap(it1->childs, tmp.childs);

		processor->deleted(from);
		processor->created(to);

		merge(processor, &tmp, from, to);
	    }
	}

	return 0;
    }


    int
    process_link(const char* path, const char* lnk, void* user)
    {
	StreamProcessor* processor = (StreamProcessor*) user;

#ifdef DEBUG_PROCESS
	y2deb("link path:'" << path << "' lnk:'" << lnk << "'");
#endif

	processor->created(path);

	return 0;
    }


    int
    process_unlink(const char* path, void* user)
    {
	StreamProcessor* processor = (StreamProcessor*) user;

#ifdef DEBUG_PROCESS
	y2deb("unlink path:'" << path << "'");
#endif

	processor->deleted(path);

	return 0;
    }


    int
    process_rmdir(const char* path, void* user)
    {
	StreamProcessor* processor = (StreamProcessor*) user;

#ifdef DEBUG_PROCESS
	y2deb("rmdir path:'" << path << "'");
#endif

	processor->deleted(path);

	return 0;
    }


    int
    process_write(const char* path, const void* data, u64 offset, u64 len, void* user)
    {
	StreamProcessor* processor = (StreamProcessor*) user;

#ifdef DEBUG_PROCESS
	y2deb("write path:'" << path << "'");
#endif

	tree_node* node = processor->files.insert(path);
	node->status |= CONTENT;

	return 0;
    }


    int
    process_clone(const char* path, u64 offset, u64 len, const u8* clone_uuid, u64 clone_ctransid,
		  const char* clone_path, u64 clone_offset, void* user)
    {
	StreamProcessor* processor = (StreamProcessor*) user;

#ifdef DEBUG_PROCESS
	y2deb("clone path:'" << path << "'");
#endif

	tree_node* node = processor->files.insert(path);
	node->status |= CONTENT;

	return 0;
    }


    int
    process_set_xattr(const char* path, const char* name, const void* data, int len, void* user)
    {
#ifdef DEBUG_PROCESS
	y2deb("set_xattr path:'" << path << "'");
#endif

#ifdef ENABLE_XATTRS
	StreamProcessor* processor = (StreamProcessor*) user;

	tree_node* node = processor->files.insert(path);
	node->status |= XATTRS;

	if (is_acl_signature(name))
	{
	    #ifdef DEBUG_PROCESS
		y2deb("adding acl flag, signature:'" << name << "'");
	    #endif
	    node->status |= ACL;
	}
#endif

	return 0;
    }


    int
    process_remove_xattr(const char* path, const char* name, void* user)
    {
#ifdef DEBUG_PROCESS
	y2deb("remove_xattr path:'" << path << "'");
#endif

#ifdef ENABLE_XATTRS
	StreamProcessor* processor = (StreamProcessor*) user;

	tree_node* node = processor->files.insert(path);
	node->status |= XATTRS;

	if (is_acl_signature(name))
	{
	    #ifdef DEBUG_PROCESS
		y2deb("adding acl flag, signature:'" << name << "'");
	    #endif
	    node->status |= ACL;
	}
#endif

	return 0;
    }


    int
    process_truncate(const char* path, u64 size, void* user)
    {
	StreamProcessor* processor = (StreamProcessor*) user;

#ifdef DEBUG_PROCESS
	y2deb("truncate path:'" << path << "' size:" << size);
#endif

	tree_node* node = processor->files.insert(path);
	node->status |= CONTENT;

	return 0;
    }


    int
    process_chmod(const char* path, u64 mode, void* user)
    {
	StreamProcessor* processor = (StreamProcessor*) user;

#ifdef DEBUG_PROCESS
	y2deb("chmod path:'" << path << "'");
#endif

	tree_node* node = processor->files.insert(path);
	node->status |= PERMISSIONS;

	return 0;
    }


    int
    process_chown(const char* path, u64 uid, u64 gid, void* user)
    {
	StreamProcessor* processor = (StreamProcessor*) user;

#ifdef DEBUG_PROCESS
	y2deb("chown path:'" << path << "'");
#endif

	tree_node* node = processor->files.insert(path);
	node->status |= OWNER | GROUP;

	return 0;
    }


    int
    process_utimes(const char* path, struct timespec* at, struct timespec* mt, struct timespec* ct,
		   void* user)
    {
	return 0;
    }


    int
    process_update_extent(const char* path, u64 offset, u64 len, void* user)
    {
	StreamProcessor* processor = (StreamProcessor*) user;

#ifdef DEBUG_PROCESS
	y2deb("update_extent path:'" << path << "'");
#endif

	tree_node* node = processor->files.insert(path);
	node->status |= CONTENT;

	return 0;
    }


    struct btrfs_send_ops send_ops = {
	/* .subvol = */ process_subvol,
	/* .snapshot = */ process_snapshot,
	/* .mkfile = */ process_mkfile,
	/* .mkdir = */ process_mkdir,
	/* .mknod = */ process_mknod,
	/* .mkfifo = */ process_mkfifo,
	/* .mksock = */ process_mksock,
	/* .symlink = */ process_symlink,
	/* .rename = */ process_rename,
	/* .link = */ process_link,
	/* .unlink = */ process_unlink,
	/* .rmdir = */ process_rmdir,
	/* .write = */ process_write,
	/* .clone = */ process_clone,
	/* .set_xattr = */ process_set_xattr,
	/* .remove_xattr = */ process_remove_xattr,
	/* .truncate = */ process_truncate,
	/* .chmod = */ process_chmod,
	/* .chown = */ process_chown,
	/* .utimes = */ process_utimes,
	/* .update_extent = */ process_update_extent,
    };


    bool
    StreamProcessor::dumper(int fd)
    {
	FdCloser fd_closer(fd);

	unsigned int iterations = 0;

	while (true)
	{
	    boost::this_thread::interruption_point();

	    // remove the fourth parameter for older versions of libbtrfs
	    int r;

#if BTRFS_LIB_VERSION < 101
	    r = btrfs_read_and_process_send_stream(fd, &send_ops, &*this, 0);
#else
	    r = btrfs_read_and_process_send_stream(fd, &send_ops, &*this, 0, 1);
#endif

	    // Only return an error when r == -ENODATA if this was the
	    // first call to btrfs_read_and_process_send_stream(). See
	    // https://github.com/openSUSE/snapper/pull/438,
	    // https://bugzilla.suse.com/show_bug.cgi?id=1111414 and
	    // the btrfs-progs source code.

	    if (r < 0 && !(r == -ENODATA && iterations > 0))
	    {
		y2err("btrfs_read_and_process_send_stream failed " << r);

		return false;
	    }

	    if (r)
	    {
		return true;
	    }

	    ++iterations;
	}
    }


    void
    StreamProcessor::do_send(u64 parent_root_id, const vector<u64>& clone_sources)
    {
	int pipefd[2];
	int r1 = pipe(pipefd);
	if (r1 < 0)
	{
	    y2err("pipe failed errno:" << errno << " (" << stringerror(errno) << ")");
	    SN_THROW(BtrfsSendReceiveException());
	}

	// Use RAII to help close fds.
	FdCloser fd0_closer(pipefd[0]);
	FdCloser fd1_closer(pipefd[1]);

	struct btrfs_ioctl_send_args io_send;
	memset(&io_send, 0, sizeof(io_send));
	io_send.send_fd = pipefd[1];
	io_send.clone_sources = const_cast<u64*>(&clone_sources.front());
	io_send.clone_sources_count = clone_sources.size();
	io_send.parent_root = parent_root_id;
	io_send.flags = BTRFS_SEND_FLAG_NO_FILE_DATA;

	boost::packaged_task<bool> pt(boost::bind(&StreamProcessor::dumper, this, pipefd[0]));
	boost::unique_future<bool> uf = pt.get_future();

	boost::thread task(boost::move(pt));

	fd0_closer.reset();

	int r2 = ioctl(dir2.fd(), BTRFS_IOC_SEND, &io_send);
	if (r2 < 0)
	{
	    y2err("send ioctl failed errno:" << errno << " (" << stringerror(errno) << ")");
	}

	fd1_closer.close();

	uf.wait();

	if (r2 < 0 || !uf.get())
	{
	    SN_THROW(BtrfsSendReceiveException());
	}
    }


    void
    StreamProcessor::process(cmpdirs_cb_t cb)
    {
	y2mil("dir1:'" << dir1.fullname() << "' dir2:'" << dir2.fullname() << "'");

	if (!is_subvolume_read_only(dir1.fd()) || !is_subvolume_read_only(dir2.fd()))
	{
	    y2err("not read-only snapshots");
	    SN_THROW(BtrfsSendReceiveException());
	}

	u64 parent_root_id = 0;
	string name1 = string(dir1.fullname(), base.fullname().size() + 1);
	if (!get_root_id(name1, &parent_root_id))
	{
	    y2err("could not resolve root_id for " << name1);
	    SN_THROW(BtrfsSendReceiveException());
	}

	vector<u64> clone_sources;
	clone_sources.push_back(parent_root_id);

	do_send(parent_root_id, clone_sources);

	files.check(&*this);
	files.result(cb);
    }


    void
    Btrfs::cmpDirs(const SDir& dir1, const SDir& dir2, cmpdirs_cb_t cb) const
    {
	y2mil("special btrfs cmpDirs");

	try
	{
	    StopWatch stopwatch;

	    const SDir subvolume(openSubvolumeDir());

	    StreamProcessor processor(subvolume, dir1, dir2);

	    processor.process(cb);

	    y2mil("stopwatch " << stopwatch << " for comparing directories");
	}
	catch (const Exception& e)
	{
	    y2err("special btrfs cmpDirs failed, " << e.what());
	    y2mil("cmpDirs fallback");

	    snapper::cmpDirs(dir1, dir2, cb);
	}
    }


#else


    void
    Btrfs::cmpDirs(const SDir& dir1, const SDir& dir2, cmpdirs_cb_t cb) const
    {
	snapper::cmpDirs(dir1, dir2, cb);
    }


#endif



#ifdef ENABLE_ROLLBACK

    bool
    Btrfs::isDefault(unsigned int num) const
    {
	bool ret = false;

	try
	{
	    SDir subvolume_dir = openSubvolumeDir();
	    subvolid_t id = get_default_id(subvolume_dir.fd());
	    if (num == 0)
	    {
		ret = get_id(subvolume_dir.fd()) == id;
	    }
	    else
	    {
		ret = get_id(openSnapshotDir(num).fd()) == id;
	    }
	}
	catch (const runtime_error& e)
	{
	    SN_THROW(IOErrorException(string("get default failed, ") + e.what()));
	}

	return ret;
    }


    std::pair<bool, unsigned int>
    Btrfs::idToNum(int fd, subvolid_t id) const
    {
	string path = get_subvolume(fd, id);

	static const regex rx("/([0-9]+)/snapshot$", regex::extended);
	smatch match;

	if (!regex_search(path, match, rx))
	    return make_pair(false, 0);

	unsigned int num = stoi(match[1].str());

	if (!checkSnapshot(num))
	    return make_pair(false, 0);

	if (get_id(openSnapshotDir(num).fd()) != id)
	    return make_pair(false, 0);

	return make_pair(true, num);
    }


    std::pair<bool, unsigned int>
    Btrfs::getDefault() const
    {
	try
	{
	    SDir subvolume_dir = openSubvolumeDir();
	    int fd = subvolume_dir.fd();

	    return idToNum(fd, get_default_id(fd));
	}
	catch (const exception& e)
	{
	    SN_THROW(IOErrorException(string("query default id failed, ") + e.what()));
	    __builtin_unreachable();
	}
    }


    void
    Btrfs::setDefault(unsigned int num) const
    {
	try
	{
	    Hooks::set_default_snapshot(Hooks::Stage::PRE_ACTION, subvolume, this, num);

	    SDir general_dir = openGeneralDir();

	    if (num == 0)
	    {
		SDir subvolume_dir = openSubvolumeDir();
		subvolid_t id = get_id(subvolume_dir.fd());
		set_default_id(general_dir.fd(), id);
	    }
	    else
	    {
		SDir snapshot_dir = openSnapshotDir(num);
		subvolid_t id = get_id(snapshot_dir.fd());
		set_default_id(general_dir.fd(), id);
	    }

	    Hooks::set_default_snapshot(Hooks::Stage::POST_ACTION, subvolume, this, num);
	}
	catch (const runtime_error& e)
	{
	    SN_THROW(IOErrorException(string("set default failed, ") + e.what()));
	}
    }


    std::pair<bool, unsigned int>
    Btrfs::getActive() const
    {
	SDir subvolume_dir = openSubvolumeDir();
	int fd = subvolume_dir.fd();

	return idToNum(fd, get_id(fd));
    }


    bool
    Btrfs::isActive(unsigned int num) const
    {
	bool ret = false;

	try
	{
	    if (num == 0)
		SN_THROW(IllegalSnapshotException());

	    SDir snapshot_dir = openSnapshotDir(num);
	    SDir subvolume_dir = openSubvolumeDir();
	    ret = get_id(snapshot_dir.fd()) == get_id(subvolume_dir.fd());
	}
	catch (const runtime_error& e)
	{
	    SN_THROW(IOErrorException(string("get active failed, ") + e.what()));
	}

	return ret;
    }

#else

    bool
    Btrfs::isDefault(unsigned int num) const
    {
	return Filesystem::isDefault(num);
    }


    std::pair<bool, unsigned int>
    Btrfs::getDefault() const
    {
	return Filesystem::getDefault();
    }


    void
    Btrfs::setDefault(unsigned int num) const
    {
	Filesystem::setDefault(num);
    }


    std::pair<bool, unsigned int>
    Btrfs::getActive() const
    {
	return Filesystem::getActive();
    }


    bool
    Btrfs::isActive(unsigned int num) const
    {
	return Filesystem::isActive(num);
    }

#endif


    void
    Btrfs::sync() const
    {
	SDir subvolume_dir = openSubvolumeDir();

	BtrfsUtils::sync(subvolume_dir.fd());

	if (!deleted_subvolids.empty())
	{
#ifdef HAVE_LIBBTRFS
	    for (subvolid_t subvolid : deleted_subvolids)
	    {
		while (does_subvolume_exist(subvolume_dir.fd(), subvolid))
		    sleep(1);
	    }
#endif

	    deleted_subvolids.clear();

	    BtrfsUtils::sync(subvolume_dir.fd());
	}
    }


#ifdef ENABLE_ROLLBACK

    void
    Btrfs::addToFstabHelper(const string& default_subvolume_name) const
    {
	string subvol_option = default_subvolume_name;
	if (!subvol_option.empty())
	    subvol_option += "/";
	subvol_option += ".snapshots";

	MntTable mnt_table(root_prefix);
	mnt_table.parse_fstab();

	libmnt_fs* root = mnt_table.find_target(subvolume, MNT_ITER_FORWARD);
	if (!root)
	    throw runtime_error("root entry not found");

	libmnt_fs* snapshots = mnt_copy_fs(NULL, root);
	if (!snapshots)
	    throw runtime_error("mnt_copy_fs failed");

	mnt_fs_set_target(snapshots, "/.snapshots");

	char* options = mnt_fs_strdup_options(snapshots);
	mnt_optstr_remove_option(&options, "defaults");
	mnt_optstr_set_option(&options, "subvol", subvol_option.c_str());
	mnt_fs_set_options(snapshots, options);
	free(options);

	mnt_table.add_fs(snapshots);
	mnt_table.replace_file();
    }


    void
    Btrfs::removeFromFstabHelper() const
    {
	MntTable mnt_table(root_prefix);
	mnt_table.parse_fstab();

	string mountpoint = (subvolume == "/" ? "" : subvolume) +  "/.snapshots";
	libmnt_fs* snapshots = mnt_table.find_target(mountpoint, MNT_ITER_FORWARD);
	if (!snapshots)
	    return;

	mnt_table.remove_fs(snapshots);
	mnt_table.replace_file();
    }

#endif
}
