/* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only */
/* Copyright (c) 2023-2025 Brett Sheffield <bacs@librecast.net> */

#define _GNU_SOURCE
#include <librecast_pvt.h>
#include <librecast/crypto.h>
#include <librecast/net.h>
#include <librecast/sync.h>
#include <librecast/mtree.h>
#include <librecast/mdex.h>
#include <libgen.h>
#include <string.h>
#include <sys/time.h>
#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif
#ifdef HAVE_UTIME_H
#include <utime.h>
#endif

#define DEBUG_SYNC(...) if (lctx->debug & LCTX_DEBUG_SYNCFILE) fprintf(lctx->stream, __VA_ARGS__)

static sem_t semjob;

struct syncjob {
	sem_t done;
	lc_ctx_t *lctx;
	lc_stat_t *stats;
	unsigned char *hash;
	unsigned char *ptr;
	ssize_t rc;
	size_t sz;
	int flags;
	int err;
};

/* wrapper for pthread_cleanup_push */
inline static void _lc_channel_part(lc_channel_t *chan)
{
	(void)lc_channel_part(chan);
}

/* return number of bits set in bitmap (Hamming Weight) */
#ifdef HAVE_RQ_OTI
static unsigned int hamm(unsigned char *map, size_t len)
{
	unsigned int c = 0;
	if (!map) return c;
	while (len--) for (char v = map[len]; v; c++) v &= v - 1;
	return c;
}

/* receive net_tree_t (either a merkle tree or directory tree hashes + metadata) */
static ssize_t lc_recvnettree(lc_ctx_t *lctx, unsigned char *hash, net_tree_t **data, lc_stat_t *stats,
		lc_sync_options_t *opt, int flags)
{
	(void)opt, (void)flags; /* unused */
	rq_oti_t oti;
	rq_scheme_t scheme;
	lc_socket_t *sock;
	lc_channel_t *chan;
	ssize_t rc = -1;
	uint64_t F;
	uint16_t T;
	int err = 0;

	/* join channel with FEC encoding. Trees are sent with OTI header */
	sock = lc_socket_new(lctx);
	if (!sock) goto err_errno;
	chan = lc_channel_hash(lctx, hash, HASHSIZE, LC_DEFAULT_FLAGS, LC_DEFAULT_PORT);
	if (!chan) goto err_socket_close;
	if (lc_channel_bind(sock, chan) == -1) { err = errno; goto err_channel_free; }
	lc_channel_coding_set(chan, lctx->coding | LC_CODE_FEC_RQ | LC_CODE_FEC_OTI);
	if (lc_channel_join(chan) == -1) { err = errno; goto err_channel_free; }

	/* peek at OTI header of first packet, allocate buffer */
	if ((rc = lc_channel_oti_peek(chan, &oti, &scheme)) == -1) { err = errno; goto err_channel_part; }
	F = rq_oti_F(oti);
	T = rq_oti_T(oti);
	if (T != LC_DEFAULT_RQ_T) { err= EBADMSG; rc = -1; goto err_channel_part; }
	*data = malloc(F);
	if (!*data) { err = errno; goto err_channel_part; }

	/* receive tree */
	rc = lc_channel_recv(chan, *data, (size_t)F, 0);
	if (rc == -1) {
		err = errno;
		free(*data);
	}
	else {
		if (stats) stats->byt_gross += rc;
		(*data)->size = be64toh((*data)->size);
		(*data)->namesz = ntohs((*data)->namesz);
		rc = (ssize_t)F;
	}

	/* clean up */
err_channel_part:
	lc_channel_part(chan);
err_channel_free:
	lc_channel_free(chan);
err_socket_close:
	lc_socket_close(sock);
err_errno:
	if (err) errno = err;
	return rc;
}

ssize_t lc_recvdir(lc_ctx_t *lctx, unsigned char *hash, net_tree_t **dir,
		lc_stat_t *stats, lc_sync_options_t *opt, int flags)
{
	ssize_t rc;
	rc = lc_recvnettree(lctx, hash, dir, stats, opt, flags);
	if (rc == -1) return -1;
	if (((*dir)->mode & S_IFMT) == S_IFDIR) return rc;
	return (errno = ENOTDIR), -1;
}

/* receive rest of multi-part tree */
static size_t lc_recvmultiparttree(lc_ctx_t *lctx, mtree_t *tree, size_t byt,
		size_t len, size_t parts, struct stat *sb, char *sharepath)
{
	lc_socket_t *sock = NULL;
	lc_channel_t *chan = NULL;
	unsigned char root[HASHSIZE] = {0};
	ssize_t rc = 0;
	uint8_t *ptr = (uint8_t *)tree->tree;
	int err = 0, ret;
	for (size_t z = 1; z < parts; z++) {
		ptr += byt; len -= byt;
		mdex_tree_hash_sb(root, HASHSIZE, tree, z, sb, sharepath);
		sock = lc_socket_new(lctx);
		if (!sock) { err = errno; break; }
		chan = lc_channel_hash(lctx, root, HASHSIZE, LC_DEFAULT_FLAGS, LC_DEFAULT_PORT);
		if (!chan) { err = errno; goto err_socket_close; }
		ret = lc_channel_bind(sock, chan);
		if (ret == -1) { err = errno; goto err_channel_free; }
		lc_channel_coding_set(chan, lctx->coding | LC_CODE_FEC_RQ | LC_CODE_FEC_OTI);
		ret = lc_channel_join(chan);
		if (ret == -1) { err = errno; goto err_channel_free; }
		byt = MIN(len, MTREE_CHUNKSIZE);
		rc = lc_channel_recv(chan, ptr, byt, 0);
		lc_channel_part(chan);
err_channel_free:
		lc_channel_free(chan);
err_socket_close:
		lc_socket_close(sock);
		if (err) break;
	}
	if (err) errno = err;
	return rc;
}

static ssize_t buildtree(ssize_t rc, lc_ctx_t *lctx, mtree_t *tree, net_tree_t *data)
{
	if (mtree_init(tree, data->size) == 0) {
		size_t byt = rc - sizeof(struct net_tree_s) - data->namesz;
		size_t len = tree->nodes * HASHSIZE + sizeof(struct net_tree_s) + data->namesz;
		size_t parts = howmany(len, MTREE_CHUNKSIZE);
		memcpy(tree->tree, data->tree + data->namesz, byt);
		len -= sizeof(struct net_tree_s) + data->namesz;
		if (parts > 1) {
			struct stat sb = {
				.st_atim.tv_sec = data->atime_s,
				.st_atim.tv_nsec = data->atime_n,
				.st_mtim.tv_sec = data->mtime_s,
				.st_mtim.tv_nsec = data->mtime_n,
				.st_mode = data->mode,
				.st_uid = data->uid,
				.st_gid = data->gid,
			};
			char *sharepath = strndup((char *)data->tree, (size_t)data->namesz);
			if (!sharepath) return -1;
			rc = lc_recvmultiparttree(lctx, tree, byt, len, parts, &sb, sharepath);
			free(sharepath);
			if (rc == -1) return -1;
		}
		rc = mtree_verify(tree);
		if (rc == -1) errno = ENOMSG;
	}
	return rc;
}

ssize_t lc_recvtree(lc_ctx_t *lctx, unsigned char *hash, mtree_t *tree, lc_stat_t *stats,
		lc_sync_options_t *opt, int flags)
{
	net_tree_t *data = NULL;
	ssize_t rc;
	int err = 0;
	rc = lc_recvnettree(lctx, hash, &data, stats, opt, flags);
	if (rc == -1) return -1;
	if ((data->mode & S_IFMT) == S_IFDIR)
		err = EISDIR;
	else if ((size_t)rc < sizeof(struct net_tree_s))
		err = ENOMSG;
	else if (buildtree(rc, lctx, tree, data) == -1)
		err = errno;
	free(data);
	if (err) errno = err;
	return rc;
}

static ssize_t lc_sendnettree(lc_ctx_t *lctx, unsigned char *hash, const net_tree_t *data, const size_t sz,
		lc_stat_t *stats, lc_sync_options_t *opt, int flags)
{
	(void)stats, (void)opt, (void)flags; /* unused */
	lc_socket_t *sock;
	lc_channel_t *chan;
	ssize_t rc;

	/* send tree with FEC encoding */
	sock = lc_socket_new(lctx);
	if (!sock) return -1;
	pthread_cleanup_push((void (*)(void *))lc_socket_close, sock);
	chan = lc_channel_hash(lctx, hash, HASHSIZE, LC_DEFAULT_FLAGS, LC_DEFAULT_PORT);
	if (!chan) { rc = -1; goto err_socket_close; }
	pthread_cleanup_push((void (*)(void *))lc_channel_free, chan);
	if (lc_channel_bind(sock, chan) == -1) { rc = -1; goto err_channel_free; }
	if ((flags & NET_LOOPBACK) && lc_socket_loop(sock, 1) == -1) {
		rc = -1; goto err_channel_free;
	}
	lc_channel_coding_set(chan, lctx->coding | LC_CODE_FEC_RQ | LC_CODE_FEC_OTI);
	rc = lc_channel_send(chan, data, sz, 0);
	while (rc > 0) rc = lc_channel_send(chan, NULL, 0, 0);

	/* clean up */
err_channel_free:
	pthread_cleanup_pop(1); /* lc_channel_free */
err_socket_close:
	pthread_cleanup_pop(1); /* lc_socket_close */
	return rc;
}

ssize_t lc_senddir(lc_ctx_t *lctx, unsigned char *hash, net_tree_t *dir,
		lc_stat_t *stats, lc_sync_options_t *opt, int flags)
{
	size_t sz = sizeof (net_tree_t) + dir->size;
	ssize_t rc;
	dir->size = htobe64(dir->size);
	rc = lc_sendnettree(lctx, hash, dir, sz, stats, opt, flags);
	return rc;
}

ssize_t lc_sendtree(lc_ctx_t *lctx, unsigned char *hash, const mtree_t *tree,
		lc_stat_t *stats, lc_sync_options_t *opt, int flags)
{
	net_tree_t *data;
	size_t hashsz = tree->nodes * HASHSIZE;
	size_t sz = sizeof (net_tree_t) + hashsz;
	ssize_t rc;

	/* prepare tree for sending */
	data = malloc(sz);
	if (!data) return -1;
	memset(data, 0, sz);
	pthread_cleanup_push(free, data);
	data->size = htobe64(tree->len);
	memcpy(data->tree, tree->tree, hashsz);

	rc = lc_sendnettree(lctx, hash, data, sz, stats, opt, flags);
	pthread_cleanup_pop(1); /* free(data) */
	return rc;
}

ssize_t lc_recvchunk(lc_ctx_t *lctx, unsigned char *hash, void *data, const size_t len,
		lc_stat_t *stats, lc_sync_options_t *opt, int flags)
{
	(void)opt;
	lc_socket_t *sock;
	lc_channel_t *chan;
	ssize_t rc;

	sock = lc_socket_new(lctx);
	if (!sock) { rc = -1; goto err_exit; }
	pthread_cleanup_push((void (*)(void *))lc_socket_close, sock);
	chan = lc_channel_hash(lctx, hash, HASHSIZE, LC_DEFAULT_FLAGS, LC_DEFAULT_PORT);
	if (!chan) { rc = -1; goto err_socket_close; }
	pthread_cleanup_push((void (*)(void *))lc_channel_free, chan);
	if (lc_channel_bind(sock, chan) == -1) { rc = -1; goto err_channel_free; }
	lc_channel_coding_set(chan, lctx->coding | LC_CODE_FEC_RQ);
	if (lc_channel_join(chan) == -1) { rc = -1; goto err_channel_free; }
	pthread_cleanup_push((void (*)(void *))_lc_channel_part, chan);
	rc = lc_channel_recv(chan, data, len, flags);
	if (stats && rc > 0) add_fetch(&stats->byt_gross, rc);
	pthread_cleanup_pop(1); /* lc_channel_part */
err_channel_free:
	pthread_cleanup_pop(1); /* lc_channel_free */
err_socket_close:
	pthread_cleanup_pop(1); /* lc_socket_close */
err_exit:
	return rc;
}

ssize_t lc_sendchunk(lc_ctx_t *lctx, unsigned char *hash, const void *data, const size_t len,
		lc_stat_t *stats, lc_sync_options_t *opt, int flags)
{
	(void)stats, (void)opt, (void)flags; /* unused */
	lc_socket_t *sock;
	lc_channel_t *chan;
	ssize_t rc;

	sock = lc_socket_new(lctx);
	if (!sock) { rc = -1; goto err_exit; }
	pthread_cleanup_push((void (*)(void *))lc_socket_close, sock);
	chan = lc_channel_hash(lctx, hash, HASHSIZE, LC_DEFAULT_FLAGS, LC_DEFAULT_PORT);
	if (!chan) { rc = -1; goto err_socket_close; }
	pthread_cleanup_push((void (*)(void *))lc_channel_free, chan);
	if (lc_channel_bind(sock, chan) == -1) { rc = -1; goto err_channel_free; }
	if ((flags & NET_LOOPBACK) && lc_socket_loop(sock, 1) == -1) {
		rc = -1; goto err_channel_free;
	}
	lc_channel_coding_set(chan, lctx->coding | LC_CODE_FEC_RQ);
	rc = lc_channel_send(chan, data, len, 0);
	while (rc > 0) rc = lc_channel_send(chan, NULL, 0, 0);
err_channel_free:
	pthread_cleanup_pop(1); /* lc_channel_free */
err_socket_close:
	pthread_cleanup_pop(1); /* lc_socket_close */
err_exit:
	return rc;
}

#ifdef HAVE_MLD
static ssize_t share_send_data(lc_share_t *share, struct in6_addr *grp, struct mdex_mem_s *mem,
		int coding)
{
	lc_socket_t *sock;
	lc_channel_t *chan;
	ssize_t rc = -1;
	sock = lc_socket_new(share->lctx);
	if (!sock) return -1;
	chan = lc_channel_init_grp(share->lctx, grp, LC_DEFAULT_PORT);
	if (!chan) goto err_socket_close;
	if (share->ifx) lc_socket_bind(sock, share->ifx);
	if (lc_channel_bind(sock, chan) == -1) goto err_channel_free;
	if ((share->flags & LC_SHARE_LOOPBACK) && lc_socket_loop(sock, 1) == -1)
		goto err_channel_free;
	lc_channel_coding_set(chan, share->lctx->coding | coding);
	rc = lc_channel_send(chan, mem->data, mem->size, 0);
	if (rc == -1 || !chan->rq) goto err_channel_free;
	if (share->stats) {
		share->stats->byt_gross += rc;
		share->stats->byt_info += mem->size;
		share->stats->pkts_io++;
	}
	for (int i = 1; i < rq_KP(chan->rq) + RQ_OVERHEAD * 2; i++) {
		rc = lc_channel_send(chan, NULL, 0, 0);
		if (rc == -1) break;
		if (share->stats) {
			share->stats->byt_gross += rc;
			share->stats->pkts_io++;
		}
	}
err_channel_free:
	lc_channel_free(chan);
err_socket_close:
	lc_socket_close(sock);
	return rc;
}
#endif /* HAVE_MLD */

static char *concat_mdex_root_file(const char *root, const char *file)
{
	size_t rlen = strlen(root);
	size_t flen = strlen(file);
	char *path = malloc(rlen + flen + 2);
	if (!path) return NULL;
	strcpy(path, root);
	path[rlen] = '/';
	strcpy(path + rlen + 1, file);
	return path;
}

static ssize_t share_send_file(lc_share_t *share, struct in6_addr *grp, mdex_entry_t *entry, int coding)
{
	lc_ctx_t *lctx = share->lctx;
	const struct mdex_file_s *file = &entry->file;
	struct mdex_mem_s mem = { .size = file->size };
	struct stat sbs = {0};
	char *smap = NULL;
	ssize_t rc = -1;
	size_t sz = 0;
	char *path = concat_mdex_root_file(share->mdex->rootdir, file->file.name);
	if (!path) return -1;
	if ((smap = lc_mmapfile(path, &sz, PROT_READ, MAP_PRIVATE, 0, &sbs)) == MAP_FAILED)
		goto err_free_path;
	mem.data = smap + file->off;
	DEBUG_SYNC("%s\n", file->file.name);
	rc = share_send_data(share, grp, &mem, coding);
	munmap(smap, sz);
err_free_path:
	free(path);
	return rc;
}

static ssize_t share_send_dir(lc_share_t *share, struct in6_addr *grp, mdex_entry_t *entry, int coding)
{
	lc_ctx_t *lctx = share->lctx;
	const struct mdex_file_s *file = &entry->file;
	net_tree_t *dir;
	struct mdex_mem_s mem = {0};
	unsigned char *tree;
	size_t namesz = strlen(file->file.name);
	size_t sz;
	ssize_t rc = -1;
	sz = sizeof(net_tree_t) + namesz + sizeof(hash_t) * file->size;
	dir = calloc(1, sz);
	if (!dir) return -1;
	if (namesz) memcpy(dir->tree, file->file.name, namesz);
	tree = dir->tree + namesz;
	if (file->size) memcpy(tree, file->file.dir, sizeof(hash_t) * file->size);
	dir->size = htobe64(sz);
	dir->mtime_s = file->file.sb->st_mtim.tv_sec;
	dir->mtime_n = file->file.sb->st_mtim.tv_nsec;
	dir->atime_s = file->file.sb->st_atim.tv_sec;
	dir->atime_n = file->file.sb->st_atim.tv_nsec;
	dir->mode = file->file.sb->st_mode;
	dir->uid = file->file.sb->st_uid;
	dir->gid = file->file.sb->st_gid;
	dir->namesz = htons(namesz);
	mem.data = dir;
	mem.size = sz;
	DEBUG_SYNC("%s\n", file->file.name);
	rc = share_send_data(share, grp, &mem, coding);
	free(dir);
	return rc;
}

/* readlink(), then pack filename + link data into dir->tree payload and set set
 * header metadata */
static ssize_t share_send_link(lc_share_t *share, struct in6_addr *grp, mdex_entry_t *entry, int coding)
{
	lc_ctx_t *lctx = share->lctx;
	const struct mdex_file_s *file = &entry->file;
	net_tree_t *dir;
	char *buf, *linkpath = NULL;
	unsigned char *tree;
	struct mdex_mem_s mem = {0};
	size_t namesz = strlen(file->file.name);
	size_t sz;
	ssize_t rc = -1;
	long int path_max;
#ifdef PATH_MAX
	path_max = PATH_MAX;
#else
	path_max = pathconf(name, _PC_PATH_MAX);
	if (path_max <= 0) path_max = 1024;
#endif
	buf = malloc(path_max);
	if (!buf) return -1;
	linkpath = concat_mdex_root_file(share->mdex->rootdir, file->file.name);
	if (!linkpath) goto err_free_buf;
	rc = readlink(linkpath, buf, (size_t)path_max);
	if (rc == -1) goto err_free_linkpath;
	sz = sizeof(net_tree_t) + namesz + (size_t)rc;
	dir = calloc(1, sz);
	if (!dir) goto err_free_linkpath;
	if (namesz) memcpy(dir->tree, file->file.name, namesz);
	tree = dir->tree + namesz;
	memcpy(tree, buf, (size_t)rc);
	dir->size = htobe64(sz);
	dir->mtime_s = file->file.sb->st_mtim.tv_sec;
	dir->mtime_n = file->file.sb->st_mtim.tv_nsec;
	dir->atime_s = file->file.sb->st_atim.tv_sec;
	dir->atime_n = file->file.sb->st_atim.tv_nsec;
	dir->mode = file->file.sb->st_mode;
	dir->uid = file->file.sb->st_uid;
	dir->gid = file->file.sb->st_gid;
	dir->namesz = htons(namesz);
	mem.data = dir;
	mem.size = sz;
	DEBUG_SYNC("%s\n", file->file.name);
	rc = share_send_data(share, grp, &mem, coding);
	free(dir);
err_free_linkpath:
	free(linkpath);
err_free_buf:
	free(buf);
	return rc;
}

static void *share_send_thread(void *arg)
{
	lc_share_t *share = lc_share_acquire((lc_share_t *)arg);
	if (!share) return NULL;
	q_job_t job = {0};
	while (1) {
		pthread_testcancel();
		if (q_wait(&share->q, &job) == -1) continue;
		if (job.arg) {
			pthread_cleanup_push(free, job.arg);
			int coding = share->lctx->coding | LC_CODE_FEC_RQ;
			struct qentry_s *qe = (struct qentry_s *)job.arg;
			mdex_entry_t *entry = &qe->entry;
			struct in6_addr *grp = &qe->grp;
			if (entry->type & MDEX_RAND) {
				coding |= LC_CODE_FEC_RAND;
			}
			if (entry->type & MDEX_OTI) {
				coding |= LC_CODE_FEC_OTI;
			}
			if (entry->type & MDEX_PTR) {
				share_send_data(share, grp, &entry->ptr, coding);
			}
			else if (entry->type & MDEX_FILE) {
				share_send_file(share, grp, entry, coding);
			}
			else if (entry->type & MDEX_DIR) {
				share_send_dir(share, grp, entry, coding);
			}
			else if (entry->type & MDEX_LINK) {
				share_send_link(share, grp, entry, coding);
			}
			pthread_cleanup_pop(1); /* free(entry) */
		}
	}
	return NULL;
}

/* when MLD detects a JOIN, check mdex for matching group, and queue for sending */
static void share_watch_callback(mld_watch_t *watch)
{
	lc_share_t *share = (lc_share_t *)mld_watch_arg(watch);
	mdex_entry_t entry = {0};
	unsigned char hash[HASHSIZE];
	struct in6_addr *grp = mld_watch_grp(watch);
	memcpy(hash, &(grp->s6_addr[2]), 14);
	if (!mdex_get(share->mdex, hash, 14, &entry)) {
		if (!q_search(&share->q, NULL, &entry)) {
			struct qentry_s *arg = malloc(sizeof (struct qentry_s));
			if (arg) {
				arg->grp = *grp;
				arg->entry = entry;
				q_push(&share->q, NULL, arg);
			}
		}
	}
}

lc_share_t * lc_share(lc_ctx_t *lctx, mdex_t *mdex, unsigned int ifx, lc_stat_t *stats,
		lc_sync_options_t *opt, int flags)
{
#ifdef HAVE_MLD
	(void)stats, (void)opt, (void)flags; /* unused */
	const int nthread_send = SYNC_SEND_THREADS;
	lc_share_t *share;
	int err = 0;

	/* allocate & initialize share */
	share = calloc(1, sizeof (lc_share_t));
	if (!share) return NULL;
	share->readers = 1;
	share->tsend = calloc(nthread_send, sizeof(pthread_t));
	if (!share->tsend) { err = errno; goto err_free_share; }
	share->lctx = lctx;
	share->stats = stats;
	share->mdex = mdex;
	share->ifx = ifx;
	share->flags = flags;

	/* initialize MLD and watch callback */
	share->mld = mld_init(0);
	if (!share->mld) { err = errno; goto err_free_tsend; }
	share->watch = mld_watch_add(share->mld, ifx, NULL, share_watch_callback, share, MLD_EVENT_JOIN);
	if (!share->watch) { err = errno; goto err_free_mld; }
	if (!mld_start(share->mld)) { err = errno; goto err_free_mld; }

	/* initialize send queue and start send thread(s) */
	if (q_init(&share->q) == -1) { err = errno; goto err_free_mld; }
	err = q_pool_create(share->tsend, nthread_send, share_send_thread, share);
	share->nthread_send = nthread_send - err; /* number of threads actually created */
	if (!share->nthread_send) { err = errno; goto err_free_q; }
	return share;
err_free_q:
	(void)q_free(&share->q);
err_free_mld:
	free(share->mld);
err_free_tsend:
	free(share->tsend);
err_free_share:
	free(share);
	if (err) errno = err;
	return NULL;
#else
	(void)lctx; (void)mdex; (void)ifx; (void)stats; (void)opt; (void)flags; /* unused */
	return (errno = ENOTSUP), NULL;
#endif /* HAVE_MLD */
}

lc_share_t *lc_share_acquire(lc_share_t *share)
{
	if (!share || aload(&share->deleted)) return NULL;
	fetch_add(&share->readers, 1); /* increase reference count */
	return share;
}

void lc_share_release(lc_share_t *share)
{
	/* decrease reference count, fetch updated value */
	int refs = add_fetch(&share->readers, -1);
	int deleted = aload(&share->deleted);
	if (!refs && deleted == DELETE_LATER) {
		/* no readers left, try to delete */
		if (CAE(&share->deleted, &share, DELETE_IN_PROGRESS)) {
			free(share);
		}
	}
}

void lc_unshare(lc_share_t *share)
{
#ifdef HAVE_MLD
	int err = errno;
	q_job_t job = {0};
	q_pool_destroy(share->tsend, share->nthread_send);
	while (share->nthread_send--) lc_share_release(share);
	free(share->tsend);
	while (!q_trywait(&share->q, &job)) free(job.arg);
	(void)q_free(&share->q);
	mld_stop(share->mld);
	mld_watch_del(share->watch);
	mld_free(share->mld);
	int deleted = 0;
	if (CAE(&share->deleted, &deleted, DELETE_LATER))
		lc_share_release(share);
	errno = err;
#else
	(void)share;
#endif
}
#endif /* HAVE_LIBLCRQ */

int lc_memsync(void *dst, void *src, const size_t n, q_t *q, lc_stat_t *stats,
		lc_sync_options_t *opt, int flags)
{
	(void)opt, (void)flags; /* unused */
	mtree_t stree = {0}, dtree = {0};
	unsigned char *map;
	int rc = -1, err = 0;

	if (!n) return (errno = EINVAL), -1;

	/* create mtree for src and dest */
	if (mtree_init(&stree, n) == -1) { err = errno; goto err_errno; }
	if (mtree_init(&dtree, n) == -1) { err = errno; goto err_free_stree; }

	/* build trees */
	if (mtree_build(&stree, src, q) == -1) { err = errno; goto err_free_dtree; }
	if (mtree_build(&dtree, dst, q) == -1) { err = errno; goto err_free_dtree; }

	/* diff trees */
	map = mtree_diff_subtree(&stree, &dtree, 0, 1);
	if (!map) { err = errno; goto err_free_dtree; }

	/* sync data */
	for (size_t z = 0; z < stree.chunks; z++) {
		if (isset(map, z)) {
			size_t off = z * MTREE_CHUNKSIZE;
			size_t len = MIN(MTREE_CHUNKSIZE, n - off);
			memcpy((char *)dst + off, (char *)src + off, len);
			if (stats) stats->byt_info += len; /* Information Rate */
		}
	}

	/* clean up & return */
	free(map);
err_free_dtree:
	rc = 0;
	mtree_free(&dtree);
err_free_stree:
	mtree_free(&stree);
err_errno:
	if (err) errno = err;
	return rc;
}

void *lc_mmapfile(const char *pathname, size_t *len, int prot, int flags, off_t offset,
		struct stat *sb)
{
	char *map = MAP_FAILED;
	mode_t mode = (sb->st_mode) ? sb->st_mode : 0600;
	int oflag = (prot & PROT_WRITE) ? O_RDWR | O_CREAT : O_RDONLY;
	int fd;
	/* if we're not creating a file, check first if this is a directory or symlink */
	if (!(prot & PROT_WRITE) && !sb->st_ino && lstat(pathname, sb) == -1) return map;
	if (!*len && !sb->st_size) return (errno = ENODATA), map;
	if ((sb->st_mode & S_IFMT) == S_IFDIR) return (errno = EISDIR), map;
	if ((sb->st_mode & S_IFMT) == S_IFLNK) return (errno = EMLINK), map;
	/* not a directory or symlink, continue with mapping */
	if ((fd = open(pathname, oflag, mode)) == -1) return map;
	if (*len && ftruncate(fd, *len) == -1) goto err_close_fd;
	if (!*len) *len = sb->st_size;
	map = mmap(NULL, *len, prot, flags, fd, offset);
err_close_fd:
	close(fd);
	return map;
}

static int utimeset(const char *pathname, struct timespec *atime, struct timespec *mtime)
{
#ifdef HAVE_UTIMENSAT
	struct timespec t[2] = {0};
	if (atime) t[0] = *atime;
	if (mtime) t[1] = *mtime;
	return utimensat(AT_FDCWD, pathname, t, AT_SYMLINK_NOFOLLOW);
#elif defined(HAVE_UTIMES)
	struct timeval t[2] = {0};
	if (atime) {
		t[0].tv_sec = atime->tv_sec;
		t[0].tv_usec = atime->tv_nsec / 1000;
	}
	if (mtime) {
		t[1].tv_sec = mtime->tv_sec;
		t[1].tv_usec = mtime->tv_nsec / 1000;
	}
	return utimes(pathname, t);
#else
	return (errno = ENOTSUP), -1;
#endif
}

static int setfilestat(const char *pathname, struct stat *sb, int flags)
{
	struct timespec *atime, *mtime;
	int uid, gid;
	int rc = 0;
	if (!sb) return (errno = EINVAL), -1;
	atime = (flags & SYNC_ATIME) ? &sb->st_atim : NULL;
	mtime = (flags & SYNC_MTIME) ? &sb->st_mtim : NULL;
	uid = (flags & SYNC_OWNER) ? (int)sb->st_uid : -1;
	gid = (flags & SYNC_GROUP) ? (int)sb->st_gid : -1;
	if ((uid | gid) != -2) rc += chown(pathname, uid, gid);
	if (flags & SYNC_MODE) rc += chmod(pathname, sb->st_mode & 0777);
	if (atime || mtime) rc += utimeset(pathname, atime, mtime);
	return rc;
}

#ifdef HAVE_RQ_OTI
static int setfilemeta(const char *pathname, net_tree_t *data, int flags)
{
	struct timespec atime = {0};
	struct timespec mtime = {0};
	int uid, gid;
	int rc = 0;
	if (flags & SYNC_ATIME) {
		atime.tv_sec = data->atime_s;
		atime.tv_nsec = data->atime_n;
	}
	if (flags & SYNC_MTIME) {
		mtime.tv_sec = data->mtime_s;
		mtime.tv_nsec = data->mtime_n;
	}
	uid = (flags & SYNC_OWNER) ? (int)data->uid : -1;
	gid = (flags & SYNC_GROUP) ? (int)data->gid : -1;
	if ((uid | gid) != -2) rc += chown(pathname, uid, gid);
	if (flags & SYNC_MODE) rc += chmod(pathname, data->mode & 0777);
	if ((flags & SYNC_ATIME) || (flags & SYNC_MTIME))
		rc += utimeset(pathname, &atime, &mtime);
	return rc;
}
#endif

static int syncdirlocal(mdex_t *sdex, mdex_t *ddex, const char *src, const char *dst, size_t top, q_t *q,
		lc_stat_t *stats, lc_sync_options_t *opt, int flags);

static int copydirlocal(mdex_t *sdex, mdex_t *ddex, mdex_hash_t sdir, size_t top, q_t *q,
		lc_stat_t *stats, lc_sync_options_t *opt, int flags)
{
	mdex_entry_t se;
	char *tmp;
	char *dname = NULL;
	char *sharepath = NULL;
	struct stat sb = {0};
	int srctype;
	int rc;
	/* first, find the source entry */
	rc = mdex_get(sdex, sdir, sizeof(mdex_hash_t), &se);
	if (rc == -1) return -1;
	srctype = se.type & 0xffff;
	if (srctype == MDEX_PTR) {
		unsigned char hash[HASHSIZE];
		mtree_t mtree;
		net_tree_t *data = se.ptr.data;
		data->size = be64toh(data->size);
		data->namesz = ntohs(data->namesz);
		tmp = strndup((char *)data->tree, (size_t)data->namesz);
		if (!tmp) return -1;
		sharepath = mdex_sharepath(sdex, tmp);
		free(tmp);
		if (!sharepath) return -1;
		rc = mtree_init(&mtree, data->size);
		if (rc == -1) goto err_free_sharepath;
		memcpy(mtree.tree, data->tree + data->namesz, (size_t)mtree.nodes * (size_t)HASHSIZE);
		sb.st_atim.tv_sec = data->atime_s;
		sb.st_atim.tv_nsec = data->atime_n;
		sb.st_mtim.tv_sec = data->mtime_s;
		sb.st_mtim.tv_nsec = data->mtime_n;
		sb.st_mode = data->mode;
		sb.st_uid = data->uid;
		sb.st_gid = data->gid;
		rc = mtree_verify(&mtree);
		if (rc != 0) goto err_free_sharepath;
		mdex_tree_hash_sb(hash, HASHSIZE, &mtree, 0, &sb, sharepath);
		rc = mdex_get(sdex, hash, HASHSIZE, &se);
		mtree_free(&mtree);
		if (rc != 0) goto err_free_sharepath;
	}
	/* build destination filename */
	if (!sharepath) sharepath = se.file.file.name;
	rc = snprintf(NULL, 0, "%s/%s", ddex->rootdir, sharepath) + 1;
	dname = malloc(rc);
	snprintf(dname, rc, "%s/%s", ddex->basedir, sharepath + top);
	switch (srctype) {
		case MDEX_DIR:
			rc = syncdirlocal(sdex, ddex, sharepath, dname, top, q, stats, opt, flags);
			break;
		case MDEX_PTR:
			rc = lc_syncfilelocal(dname, sharepath, q, stats, opt, flags);
			break;
		default:
			free(dname);
			return (errno = ENOTSUP), -1;
	}
	setfilestat(dname, se.file.file.sb, flags);
	free(dname);
	return rc;
err_free_sharepath:
	free(sharepath);
	return -1;
}

static int syncdirlocal(mdex_t *sdex, mdex_t *ddex, const char *src, const char *dst, size_t top, q_t *q,
		lc_stat_t *stats, lc_sync_options_t *opt, int flags)
{
	mdex_hash_t *sdir, *ddir = NULL;
	mdex_entry_t sentry, dentry = {0};
	int rc = mdex_getalias(sdex, src, &sentry);
	if (rc == -1) return -1;
	sdir = sentry.file.file.dir;
	rc = mdex_getalias(ddex, dst, &dentry);
	if (rc == -1) {
		/* destination doesn't exist. Create */
		rc = mkdir(dst, 0755);
		if (rc == -1) return -1;
	}
	else ddir = dentry.file.file.dir;
	if (sdir == NULL && ddir == NULL) return 0;
	if (sentry.file.size == dentry.file.size) {
		size_t sz = sentry.file.size * sizeof(*sdir);
		if (!memcmp(sdir, ddir, sz)) return 0; /* directories match */
	}

	/* sync each directory entry */
	for (size_t i = 0; i < sentry.file.size; i++) {
		int found = 0;
		if (ddir) for (size_t j = 0; j < sentry.file.size; j++) {
			if (!memcmp(sdir[i], ddir[j], sizeof(mdex_hash_t))) {
				found = 1; break;
			}
		}
		if (!found) {
			rc = copydirlocal(sdex, ddex, sdir[i], top, q, stats, opt, flags);
			if (rc == -1) return -1;
		}
	}
	rc = setfilestat(dst, sentry.file.file.sb, flags);
	return 0;
}

static int syncrecursive_local(const char *dst, const char *src, q_t *q,
		lc_stat_t *stats, lc_sync_options_t *opt, int flags)
{
	(void)stats, (void)opt; /* unused */
	mdex_t *sdex, *ddex;
	int err = 0;
	int rc = -1;
	int mflags = 0;

	if (flags & SYNC_RECURSE) mflags |= MDEX_RECURSE;

	/* index source directory */
	sdex = mdex_init(512);
	if (!sdex) return -1;
	rc = mdex_addfile(sdex, src, q, mflags);
	if (rc) { err = errno; goto err_sdex_free; }

	/* index destination directory */
	ddex = mdex_init(512);
	if (!ddex) { err = errno; goto err_sdex_free; }
	rc = mdex_addfile(ddex, dst, q, mflags);
	if (rc) { err = errno; goto err_ddex_free; }
	rc = syncdirlocal(sdex, ddex, src, dst, sdex->basedirlen - sdex->rootdirlen, q, stats, opt, flags);
err_ddex_free:
	mdex_free(ddex);
err_sdex_free:
	mdex_free(sdex);
	if (err) errno = err;
	return rc;
}

static char *mkfilename(const char *dir, const char *file)
{
	char *filename;
	int rc;
	rc = snprintf(NULL, 0, "%s/%s", dir, file) + 1;
	filename = malloc(rc);
	if (!filename) return NULL;
	snprintf(filename, rc, "%s/%s", dir, file);
	return filename;
}

int lc_syncfilelocal(const char *dst, char *src, q_t *q,
		lc_stat_t *stats, lc_sync_options_t *opt, int flags)
{
	(void)stats, (void)opt, (void)flags; /* unused */
	struct stat sbs = {0}, sbd = {0};
	char *smap, *dmap, *fname = NULL;
	const char *ddst = dst;
	size_t sz = 0;
	int rc = -1, err = 0;

	/* process and strip trailing slash */
	sz = strlen(src);
	if (!sz) return (errno = EINVAL), -1;
	sz--;
	/* no trailing slash => create subdirectory */
	if (src[sz] != '/') flags |= SYNC_SUBDIR;
	else src[sz] = '\0'; /* trim slash */
	sz = 0;

	smap = lc_mmapfile(src, &sz, PROT_READ, MAP_PRIVATE, 0, &sbs);
	if (smap == MAP_FAILED) {
		switch (errno) {
			case EISDIR:
				return syncrecursive_local(dst, src, q, stats, opt, flags);
			case ENODATA:
				rc = creat(ddst, sbs.st_mode & 0777);
				if (rc != -1) {
					close(rc);
					goto err_errno;
				}
				/* fallthru */
			default:
				err = errno; goto err_errno;
		}
	}
	sbd.st_mode = sbs.st_mode;
	sz = sbs.st_size;
remap_ddst:
	dmap = lc_mmapfile(ddst, &sz, PROT_READ|PROT_WRITE, MAP_SHARED, 0, &sbd);
	if (dmap == MAP_FAILED) {
		if (errno == EISDIR) {
			char *tmp = strdup(src);
			fname = mkfilename(dst, basename(tmp));
			free(tmp);
			ddst = fname;
			goto remap_ddst;
		}
		else { err = errno; goto err_free_smap; }
	}
	if (sbs.st_size > sbd.st_size) {
		/* source is longer, copy extra bytes before syncing first part */
		size_t ext = sbs.st_size - sbd.st_size;
		memcpy(dmap + sbs.st_size - ext, smap + sbs.st_size - ext, ext);
		if (stats) stats->byt_info += ext; /* Information Rate */
	}
	rc = lc_memsync(dmap, smap, sz, q, stats, opt, flags);
	msync(dmap, sz, MS_SYNC); /* on OpenBSD an explicit msync() is required */
	munmap(dmap, sz);
err_free_smap:
	munmap(smap, sz);
err_errno:
	setfilestat(ddst, &sbs, flags);
	if (fname) free(fname);
	if (err) errno = err;
	return rc;
}

#ifdef HAVE_RQ_OTI
ssize_t lc_sync(lc_ctx_t *lctx, unsigned char *hash, void *data, const size_t len,
		q_t *q, lc_stat_t *stats, lc_sync_options_t *opt, int flags)
{
	(void)stats, (void)opt, (void)flags; /* unused */
	mtree_t stree = {0};
	mtree_t dtree = {0};
	unsigned char *map;
	ssize_t byt = 0, rc;
	if ((rc = lc_recvtree(lctx, hash, &stree, stats, opt, flags)) == -1) return -1;
	if ((rc = mtree_init(&dtree, len) == -1)) goto err_free_stree;
	if ((rc = mtree_build(&dtree, data, q) == -1)) goto err_free_dtree;
	map = mtree_diff_subtree(&stree, &dtree, 0, 1);
	if (map) {
		size_t min = mtree_subtree_data_min(stree.base, 0);
		unsigned char *ptr;
		for (size_t z = 0; z < stree.chunks; z++) {
			if (isset(map, z)) {
				size_t off = z * MTREE_CHUNKSIZE;
				size_t sz = MIN(MTREE_CHUNKSIZE, len - off);
				ptr = (unsigned char *)data + off;
				unsigned char *chunkhash = mtree_nnode(&stree, min + z);
				rc = lc_recvchunk(lctx, chunkhash, ptr, sz, stats, opt, flags);
				if (rc == -1) break;
				byt += rc;
			}
		}
		free(map);
		if (rc != -1) rc = byt;
	}
err_free_dtree:
	mtree_free(&dtree);
err_free_stree:
	mtree_free(&stree);
	return rc;
}

static void * sync_chunk(void *arg)
{
	struct syncjob *job = (struct syncjob *)arg;
	unsigned char hash[HASHSIZE];
	do {
		job->rc = lc_recvchunk(job->lctx, job->hash, job->ptr, job->sz, job->stats, NULL, job->flags);
		hash_generic(hash, sizeof hash, job->ptr, job->sz);
	}
	while (memcmp(hash, job->hash, HASHSIZE));
	sem_post(&job->done);
	sem_post(&semjob);
	return NULL;
}

inline static ssize_t syncfile(lc_ctx_t *lctx, unsigned char *map, mtree_t *stree, char **dst,
		size_t len, q_t *q, lc_stat_t *stats, lc_sync_options_t *opt, int flags)
{
	(void)opt; /* unused */
	struct syncjob *job;
	ssize_t byt = 0, rc = 0;
	size_t min = mtree_subtree_data_min(stree->base, 0);
	int diffs = hamm(map, stree->chunks);
	int err = 0, j = 0;
	job = calloc(diffs, sizeof(struct syncjob));
	if (!job) return -1;
	/* queue jobs to sync each differing chunk */
	sem_init(&semjob, 0, SYNC_MAX_RECVERS);
	for (size_t z = 0; z < stree->chunks; z++) {
		if (isset(map, z)) {
			sem_wait(&semjob);
			size_t off = z * MTREE_CHUNKSIZE;
			job[j].lctx = lctx;
			job[j].sz = MIN(MTREE_CHUNKSIZE, len - off);
			job[j].ptr = (unsigned char *)dst + off;
			job[j].hash = mtree_nnode(stree, min + z);
			job[j].stats = stats;
			job[j].flags = flags;
			sem_init(&job[j].done, 0, 0);
			q_push(q, sync_chunk, &job[j++]);
		}
	}
	/* reap jobs, gather stats */
	for (int i = 0; i < diffs; i++) {
		sem_wait(&job[i].done);
		sem_destroy(&job[i].done);
		if (job[i].rc > 0) {
			/* Gross rate set by lc_recvtree & lc_recvchunk */
			if (stats) stats->byt_info += job[i].sz; /* Information Rate */
			byt += job[i].sz;
		}
		else {
			rc = -1;
			err = job->err;
		}
	}
	sem_destroy(&semjob);
	if (rc != -1) rc = byt;
	else err = errno;
	free(job);
	if (err) errno = err;
	return rc;
}

static char *mkentryname(const char *pathname, net_tree_t *data, size_t trim)
{
	char *name = NULL;
	int rc;
	if (trim >= data->namesz) return NULL;
	rc = snprintf(NULL, 0, "%s/%.*s", pathname, (int)(data->namesz - trim), data->tree + trim);
	name = malloc(rc + 1);
	if (!name) return NULL;
	snprintf(name, rc + 1, "%s/%.*s", pathname, (int)(data->namesz - trim), data->tree + trim);
	return name;
}

static char *mksubdir(lc_ctx_t *lctx, const char *pathname, net_tree_t *data, size_t trim)
{
	char *subdir = NULL;
	int rc;
	subdir = mkentryname(pathname, data, trim);
	if (!subdir) return NULL;
	rc = mkdir(subdir, 0755); /* create writable, will set mode later */
	if (rc == -1) goto err_free_subdir;
	DEBUG_SYNC("%s\n", subdir);
	return subdir;
err_free_subdir:
	rc = errno;
	free(subdir);
	errno = rc;
	return NULL;
}

static ssize_t synclink(lc_ctx_t *lctx, const char *pathname, net_tree_t *data,
		int flags, size_t trim, int level)
{
	const char *linkpath;
	char *lpath = NULL;
	char *target;
	char *ptr;
	size_t targetsz = data->size - data->namesz - sizeof(net_tree_t);
	int rc;

	if (level && flags & SYNC_SUBDIR) {
		if (data->namesz) {
			lpath = mkentryname(pathname, data, trim);
			if (!lpath) return -1;
			linkpath = lpath;
		}
		else return (errno = EBADMSG), -1;
	}
	else linkpath = pathname;
	ptr = (char *)(data->tree + data->namesz);
	target = strndup(ptr, targetsz);
	rc = symlink(target, linkpath);
	if (rc == -1) {
		if (errno == EEXIST) rc = 0;
	}
	else DEBUG_SYNC("%s\n", linkpath);
	/* XXX: do not set mode for symlinks */
	if (!rc) setfilemeta(linkpath, data, (flags | SYNC_MODE) ^ SYNC_MODE);
	free(target);
	free(lpath);
	return rc;
}

static ssize_t syncdir(lc_ctx_t *lctx, mdex_t *ddex, const char *pathname, net_tree_t *data,
		q_t *q, lc_stat_t *stats, lc_sync_options_t *opt, int flags, size_t trim)
{
	ssize_t byt = 0;
	ssize_t rc = 0;
	char *subdir = NULL;
	if (!data->namesz) return -1;
	if (flags & SYNC_SUBDIR) subdir = mksubdir(lctx, pathname, data, trim);
	else flags |= SYNC_SUBDIR;
	if (flags & SYNC_RECURSE) {
		unsigned char *hash = data->tree + data->namesz;
		int entries = (data->size - data->namesz - sizeof(net_tree_t)) / HASHSIZE;
		for (int i = 0; i < entries; i++) {
			if (mdex_get(ddex, hash, HASHSIZE, NULL)) {
				rc = lc_syncfile(lctx, hash, pathname, q, stats, opt, flags);
				if (rc == -1) break;
			}
			byt += rc;
			hash += HASHSIZE;
		}
	}
	if (subdir) {
		setfilemeta(subdir, data, flags);
		free(subdir);
	}
	else setfilemeta(pathname, data, flags);
	return (rc == -1) ? -1 : byt;
}

static mdex_t *index_dest(lc_ctx_t *lctx, const char *pathname, net_tree_t *data, q_t *q, int trim)
{
	mdex_t *ddex;
	char *subdir;
	int rc;
	ddex = mdex_init(0);
	if (!ddex) return NULL;
	subdir = mkentryname(pathname, data, trim);
	DEBUG_SYNC("hashing directory %s... ", subdir);
	rc = mdex_addfile(ddex, subdir, q, MDEX_RECURSE);
	if (rc == -1) goto err_free_subdir;
	DEBUG_SYNC("done.\n");
	free(subdir);
	return ddex;
err_free_subdir:
	rc = errno;
	free(subdir);
	errno = rc;
	mdex_free(ddex);
	return NULL;
}

ssize_t lc_syncfile(lc_ctx_t *lctx, unsigned char *hash, const char *pathname,
		q_t *q, lc_stat_t *stats, lc_sync_options_t *opt, int flags)
{
	mtree_t stree = {0}, dtree = {0};
	static mdex_t *ddex; /* destination mdex */
	struct stat sbd = {0};
	char **dst = NULL;
	char *fname = NULL;
	const char *mapfile = pathname;
	unsigned char *map;
	pthread_t *tid;
	ssize_t byt = 0, rc;
	size_t len = 0;
	int err = 0, qfree = 0, nthreads;
	static size_t trim;
	static int level;

	net_tree_t *data = NULL;
	if ((rc = lc_recvnettree(lctx, hash, &data, stats, opt, flags)) == -1) return -1;
	byt += rc;
	if ((size_t)rc < sizeof(struct net_tree_s)) err = ENOMSG;
	else switch (data->mode & S_IFMT) {
		case S_IFDIR:
			if (!(flags & SYNC_SUBDIR)) {
				trim = ((opt && opt->sharelen) ? opt->sharelen : data->namesz) + 1;
			}
			else if (opt) trim = opt->trim;
			if (!level && (flags & SYNC_RECURSE)) {
				ddex = index_dest(lctx, pathname, data, q, trim);
			}
			level++;
			rc = syncdir(lctx, ddex, pathname, data, q, stats, opt, flags, trim);
			level--;
			goto err_return;
		case S_IFLNK:
			rc = synclink(lctx, pathname, data, flags, trim, level);
			goto err_return;
		case S_IFREG:
		default:
			if (buildtree(rc, lctx, &stree, data) == -1) err = ENOMSG;
	}
	if (err) goto err_return;
	nthreads = MIN(MTREE_THREADMAX, stree.base);
	tid = calloc(nthreads, sizeof(pthread_t));
	if (!tid) {
		err = errno;
		rc = -1;
		goto err_free_stree;
	}
	if (!q) { /* no queue supplied, allocate one */
		if (!(q = calloc(1, sizeof *q))) { err = errno; goto err_free_tid; }
		qfree |= 1;
		if (q_init(q) == -1) { err = errno; goto err_free_q; }
		qfree |= 2;
		if (q_pool_create(tid, nthreads, q_job_seek, q) != 0) {
			err = errno; goto err_free_q;
		}
		qfree |= 4;
	}
	len = stree.len;
	if (level && flags & SYNC_SUBDIR) {
		if (data->namesz) {
			fname = mkentryname(pathname, data, trim);
			if (!fname) goto err_free_q;
			mapfile = fname;
		}
		else {
			errno = EBADMSG, rc = -1;
			goto err_free_q;
		}
	}
	DEBUG_SYNC("%s\n", mapfile);
	if (!len) {
		/* zero-length file */
		rc = creat(mapfile, 0755);
		if (rc == -1) {
			errno = err;
			goto err_free_q;
		}
		close(rc);
		goto do_setfilemeta;
	}
	dst = lc_mmapfile(mapfile, &len, PROT_READ|PROT_WRITE, MAP_SHARED, 0, &sbd);
	if (dst == MAP_FAILED) {
		err = errno;
		rc = -1;
		goto err_free_q;
	}
	if (mtree_init(&dtree, len) == -1) {
		err = errno;
		rc = -1;
		goto err_free_dst;
	}
	if (mtree_build(&dtree, dst, q) == -1) {
		err = errno;
		rc = -1;
		goto err_free_dtree;
	}
	map = mtree_diff_subtree(&stree, &dtree, 0, 1);
	if (map) {
		rc = syncfile(lctx, map, &stree, dst, len, q, stats, opt, 0);
		if (rc != -1) {
			rc = byt;
		}
		else err = errno;
		msync(dst, len, MS_SYNC); /* on OpenBSD an explicit msync() is required */
		free(map);
	}
	/* Clean up. My goodness we've made such an awful mess. Sorry Mum! */
err_free_dtree:
	mtree_free(&dtree);
err_free_dst:
	munmap(dst, len);
do_setfilemeta:
	if (rc != -1 && data) setfilemeta(mapfile, data, flags);
err_free_q:
	free(fname);
	if (qfree) {
		if (qfree & 4) q_pool_destroy(tid, nthreads);
		if (qfree & 2) q_free(q);
		if (qfree & 1) free(q);
	}
err_free_tid:
	free(tid);
err_free_stree:
	mtree_free(&stree);
err_return:
	if (err) errno = err;
	free(data);
	if (!level && ddex) mdex_free(ddex);
	return (rc == -1) ? -1 : byt;
}

/* process trailing slash and hash src before calling lc_syncfile */
ssize_t lc_syncfile_hash(lc_ctx_t *lctx, char *src, const char *dst,
		q_t *q, lc_stat_t *stats, lc_sync_options_t *opt, int flags)
{
	unsigned char hash[HASHSIZE];
	size_t len = strlen(src);
	ssize_t rc = -1;
	int optalloc = 0;
	if (!len) return (errno = EINVAL), -1;
	if (!opt) {
		opt = calloc(1, sizeof(lc_sync_options_t));
		optalloc++;
	}
	if (!opt) return -1;
	if (src[len - 1] != '/') {
		/* no trailing slash => create subdirectory */
		flags |= SYNC_SUBDIR;
		char *tmp = strdup(src);
		if (!tmp) goto err_free_opt;
		opt->trim = strlen(basename(tmp));
		free(tmp);
	}
	else {
		src[--len] = '\0'; /* trim slash */
		opt->trim = -1;
	}
	hash_generic(hash, sizeof hash, (unsigned char *)src, len);
	opt->share = src;
	opt->sharelen = strlen(opt->share);
	opt->trim = opt->sharelen - opt->trim;
	rc = lc_syncfile(lctx, hash, dst, q, stats, opt, flags);
err_free_opt:
	if (optalloc) free(opt);
	return rc;
}
#endif /* HAVE_LIBLCRQ */
