/* SPDX-License-Identifier: ISC */
#include "config.h"

#include <getopt.h>
#include <pwd.h>
#include <grp.h>
#include <stdio.h>
#include <stdarg.h>
#include <libgen.h>

#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <sysrepo.h>
#include <sysrepo/netconf_acm.h>
#include <sysrepo/values.h>

#include "util.h"

#define err(rc, fmt, args...)      { fprintf(stderr, ERRMSG fmt ":%s\n", ##args, strerror(errno)); exit(rc); }
#define errx(rc, fmt, args...)     { fprintf(stderr, ERRMSG fmt "\n", ##args);                     exit(rc); }
#define warnx(fmt, args...)          fprintf(stderr, ERRMSG fmt "\n", ##args)
#define warn(fmt, args...)           fprintf(stderr, ERRMSG fmt ":%s\n", ##args, strerror(errno))
#define dbg(fmt, args...) if (debug) fprintf(stderr, DBGMSG fmt "\n", ##args)

struct infix_ds {
	char *name;		/* startup-config, etc.  */
	int   datastore;	/* sr_datastore_t and -1 */
	bool  rw;		/* read-write:1 or not:0 */
	char *path;		/* local path or NULL    */
};

const struct infix_ds infix_config[] = {
	{ "startup-config",    SR_DS_STARTUP,         true, "/cfg/startup-config.cfg" },
	{ "running-config",    SR_DS_RUNNING,         true, NULL },
	/* { "candidate-config",  SR_DS_CANDIDATE,       true, NULL }, */
	{ "operational-state", SR_DS_OPERATIONAL,     false, NULL },
	{ "factory-config",    SR_DS_FACTORY_DEFAULT, false, NULL }
};

static const char *prognm;
static const char *remote_user;
static char *xpath = "/*";
static int debug;
static int force;
static int timeout;
static int dry_run;
static int sanitize;

/*
 * Current system user, same as sysrepo user.  We use getuid() here
 * because `copy` is SUID root to work around sysrepo issues with a
 * /dev/shm that's moounted 01777.
 */
static const char *getuser(void)
{
	const struct passwd *pw;
	uid_t uid = getuid();

	pw = getpwuid(uid);
	if (!pw)
		err(1, "failed querying user info for uid %d", uid);

	return pw->pw_name;
}

/*
 * If UNIX user is in UNIX group of directory containing file,
 * return 1, otherwise 0.
 *
 * E.g., writing to /cfg/foo, where /cfg is owned by root:wheel,
 * should result in the file being owned by $LOGNAME:wheel with
 * 0660 perms for other users in same group.
 */
static int in_group(const char *user, const char *fn, gid_t *gid)
{
	char path[PATH_MAX];
	const struct passwd *pw;
	int i, num = 0, rc = 0;
	struct stat st;
	gid_t *groups;
	char *dir;

	pw = getpwnam(user);
	if (!pw)
		return 0;

	strlcpy(path, fn, sizeof(path));
	dir = dirname(path);

	if (stat(dir, &st))
		return 0;

	num = NGROUPS_MAX;
	groups = malloc(num * sizeof(gid_t));
	if (!groups) {
		warn("failed in_group()");
		return 0;
	}

	getgrouplist(user, pw->pw_gid, groups, &num);
	for (i = 0; i < num; i++) {
		if (groups[i] == st.st_gid) {
			*gid = st.st_gid;
			rc = 1;
			break;
		}
	}
	free(groups);

	return rc;
}

/*
 * Set group owner so other users with same directory permissions can
 * read/write the file as well.  E.g., an 'admin' level user in group
 * 'wheel' writing a new file to `/cfg` should be possible to read and
 * write to by other administrators.
 *
 * This function is called only when the file has been successfully
 * copied or created in a file system directory.  This is why we can
 * safely ignore any EPERM errors to chown(), below, because if the file
 * already existed, created by another user, we are not allowed to chgrp
 * it.  The sole purpose of this function is to allow other users in the
 * same group to access the file in the future.
 */
static void set_owner(const char *fn, const char *user)
{
	gid_t gid = 9999;

	if (!fn)
		return;	/* not an error, e.g., running-config is not a file */

	if (!in_group(user, fn, &gid))
		return;	/* user not in parent directory's group */

	if (chown(fn, -1, gid) && errno != EPERM) {
		const struct group *gr = getgrgid(gid);

		warn("setting group owner %s (%d) on %s",
		     gr ? gr->gr_name : "<unknown>", gid, fn);
	}
}

static const char *infix_ds(const char *text, const struct infix_ds **ds)
{
	size_t i, len = strlen(text);

	for (i = 0; i < NELEMS(infix_config); i++) {
		if (!strncmp(infix_config[i].name, text, len)) {
			*ds = &infix_config[i];
			return infix_config[i].name;
		}
	}

	*ds = NULL;
	return text;
}

static bool is_uri(const char *str)
{
	return strstr(str, "://") != NULL;
}

static bool is_ssh_uri(const char *uri)
{
	return !strncmp(uri, "scp://", 6) || !strncmp(uri, "sftp://", 7);
}

/*
 * Parse scp:// or sftp:// URI into an scp(1)-compatible remote string
 * of the form [user@]host:path, and optionally extract the port.
 *
 * URI format: scheme://[user@]host[:port]/path
 *
 * Returns a heap-allocated string the caller must free, and sets *portp
 * to a heap-allocated port string (or NULL) the caller must also free.
 * Returns NULL on parse failure.
 */
static char *ssh_uri_to_remote(const char *uri, char **portp)
{
	const char *p, *slash, *path;
	char auth[256], *auth_at, *auth_colon, *host_start;
	char *user, *remote = NULL, *port = NULL;
	size_t auth_len;

	*portp = NULL;

	if (!strncmp(uri, "scp://", 6))
		p = uri + 6;
	else if (!strncmp(uri, "sftp://", 7))
		p = uri + 7;
	else
		return NULL;

	slash = strchr(p, '/');
	if (!slash)
		slash = p + strlen(p);

	auth_len = (size_t)(slash - p);
	if (auth_len >= sizeof(auth))
		return NULL;
	memcpy(auth, p, auth_len);
	auth[auth_len] = '\0';

	auth_at = strchr(auth, '@');
	if (auth_at) {
		*auth_at = '\0';
		user = auth;
		host_start = auth_at + 1;
	} else {
		user = (char *)remote_user; /* may be NULL */
		host_start = auth;
	}

	auth_colon = strchr(host_start, ':');
	if (auth_colon) {
		*auth_colon = '\0';
		port = strdup(auth_colon + 1);
		if (!port)
			return NULL;
	}

	path = *slash ? slash : "/";

	if (user)
		(void)asprintf(&remote, "%s@%s:%s", user, host_start, path);
	else
		(void)asprintf(&remote, "%s:%s", host_start, path);

	*portp = port;
	return remote;
}

static bool is_stdout(const char *path)
{
	if (!path)
		return 1;

	return  !strcmp(path, "-") ||
		!strcmp(path, "/dev/stdout") ||
		!strcmp(path, "/dev/fd/1");
}

static char *mktmp(void)
{
	mode_t oldmask;
	char *path;
	int fd;

	path = strdup("/tmp/copy-XXXXXX");
	if (!path)
		goto err;

	oldmask = umask(0077);
	fd = mkstemp(path);
	umask(oldmask);

	if (fd < 0)
		goto err;

	if (chown(path, getuid(), -1))
		dbg("Failed to chown %s: %s", path, strerror(errno));

	close(fd);
	return path;
err:
	free(path);
	return NULL;
}

static void rmtmp(const char *path)
{
	if (remove(path)) {
		if (errno == ENOENT)
			return;

		warn("failed removing temporary file %s", path);
	}
}

static void sysrepo_print_error(sr_session_ctx_t *sess)
{
	const sr_error_info_t *erri = NULL;
	int err;

	if (!sess)
		return;

	err = sr_session_get_error(sess, &erri);
	if (err || !erri || !erri->err_count)
		return;

	warnx("%s (%d)", erri->err->message, erri->err->err_code);
}

/* Connect to sysrepo and create NACM-aware session on running datastore */
static int sysrepo_init(sr_conn_ctx_t **conn, sr_session_ctx_t **sess,
				    sr_subscription_ctx_t **sub)
{
	const char *user = getuser();
	int err;

	err = sr_connect(SR_CONN_DEFAULT, conn);
	if (err != SR_ERR_OK) {
		warnx("failed connecting to sysrepo: %s", sr_strerror(err));
		return err;
	}

	/* Always open running, because sr_nacm_init() does not work
	 * against the factory DS.
	 */
	err = sr_session_start(*conn, SR_DS_RUNNING, sess);
	if (err != SR_ERR_OK) {
		warnx("failed starting session: %s", sr_strerror(err));
		goto fail;
	}

	err = sr_nacm_init(*sess, 0, sub);
	if (err != SR_ERR_OK) {
		warnx("NACM init failed: %s", sr_strerror(err));
		goto fail;
	}

	dbg("Setting NACM user %s for session", user);
	err = sr_nacm_set_user(*sess, user);
	if (err != SR_ERR_OK) {
		warnx("NACM setup failed for user %s: %s", user, sr_strerror(err));
		goto fail;
	}

	return SR_ERR_OK;
fail:
	sysrepo_print_error(*sess);
	sr_session_stop(*sess);
	sr_disconnect(*conn);

	return err;
}

static sr_session_ctx_t *sysrepo_session(const struct infix_ds *ds)
{
	static sr_subscription_ctx_t *sub = NULL;
	static sr_session_ctx_t *sess;
	sr_conn_ctx_t *conn = NULL;
	int err;

	if (!ds) {
		if (!sess)
			return NULL;

		conn = sr_session_get_connection(sess);
		sr_session_stop(sess);
		sr_disconnect(conn);
		sess = NULL;
		sub = NULL;
		return NULL;
	}

	if (!sess) {
		err = sysrepo_init(&conn, &sess, &sub);
		if (err != SR_ERR_OK) {
			warnx("Failed to initialize session for %s", ds->name);
			return NULL;
		}
	}

	err = sr_session_switch_ds(sess, ds->datastore);
	if (err) {
		sysrepo_print_error(sess);
		warnx("%s activation failed", ds->name);
		return NULL;
	}

	return sess;
}

static int sysrepo_export(const struct infix_ds *ds, const char *path)
{
	sr_session_ctx_t *sess;
	sr_data_t *data = NULL;
	int err;

	sess = sysrepo_session(ds);
	if (!sess)
		return 1;

	err = sr_get_data(sess, xpath, 0, timeout * 1000, SR_OPER_DEFAULT, &data);
	if (err) {
		sysrepo_print_error(sess);
		warnx("failed retrieving %s data", ds->name);
		return err;
	}

	if (!data)
		return 0;

	err = lyd_print_path(path, data->tree, LYD_JSON, LYD_PRINT_SIBLINGS);
	sr_release_data(data);

	if (err) {
		sysrepo_print_error(sess);
		warnx("failed storing %s data", ds->name);
	}

	return err;
}

static int sysrepo_import(const struct infix_ds *ds, const char *path)
{
	const struct ly_ctx *ly;
	sr_session_ctx_t *sess;
	struct lyd_node *data;
	int err;

	sess = sysrepo_session(ds);
	if (!sess)
		return 1;

	ly = sr_acquire_context(sr_session_get_connection(sess));

	err = lyd_parse_data_path(ly, path, LYD_JSON,
				  LYD_PARSE_NO_STATE | LYD_PARSE_ONLY |
				  LYD_PARSE_STORE_ONLY | LYD_PARSE_STRICT, 0, &data);
	if (err) {
		warnx("failed parsing %s data", ds->name);
		goto out;
	}

	err = dry_run ? 0 : sr_replace_config(sess, NULL, data, timeout * 1000);
	if (err) {
		sysrepo_print_error(sess);
		warnx("failed importing %s data, error %d", ds->name, err);
	}

out:
	sr_release_context(sr_session_get_connection(sess));
	return err ? 1 : 0;
	/* return sysrepo_do(sysrepo_import_op, ds, path) ? 1 : 0; */
}

static int subprocess(char * const *argv)
{
	int pid, status;

	pid = fork();
	if (!pid) {
		execvp(argv[0], argv);
		exit(1);
	}

	if (pid < 0)
		return 1;

	if (waitpid(pid, &status, 0) < 0)
		return 1;

	if (!WIFEXITED(status))
		return 1;

	return WEXITSTATUS(status);
}

static int curl(char *op, const char *path, const char *uri)
{
	char *argv[10] = { "curl", "-L", NULL };
	int err = 1, i = 2;
	int path_i, uri_i = 0, user_i = 0;

	argv[i++] = op;

	path_i = i;
	argv[i] = strdup(path);
	if (!argv[i++])
		goto out;

	uri_i = i;
	argv[i] = strdup(uri);
	if (!argv[i++])
		goto out;

	if (remote_user) {
		argv[i++] = "-u";
		user_i = i;
		argv[i] = strdup(remote_user);
		if (!argv[i++])
			goto out;
	}

	err = subprocess(argv);

out:
	free(argv[path_i]);
	if (uri_i)
		free(argv[uri_i]);
	if (user_i)
		free(argv[user_i]);
	return err;
}

static int curl_upload(const char *srcpath, const char *uri)
{
	char upload[] = "-T";

	if (curl(upload, srcpath, uri)) {
		warnx("upload to %s failed", uri);
		return 1;
	}

	return 0;
}

static int curl_download(const char *uri, const char *dstpath)
{
	char download[] = "-o";
	int err;

	if ((err = curl(download, dstpath, uri))) {
		warnx("download of %s failed, exit code %d", uri, err);
		return 1;
	}

	return 0;
}

static int scp_upload(const char *srcpath, const char *uri)
{
	char *argv[12] = { "scp", "-o", "StrictHostKeyChecking=no", NULL };
	int i = 3, err = 1;
	char *remote = NULL, *port = NULL, *src_dup = NULL;

	remote = ssh_uri_to_remote(uri, &port);
	if (!remote) {
		warnx("failed to parse URI: %s", uri);
		goto out;
	}

	if (port) {
		argv[i++] = "-P";
		argv[i++] = port;
	}

	src_dup = strdup(srcpath);
	if (!src_dup)
		goto out;
	argv[i++] = src_dup;
	argv[i++] = remote;

	err = subprocess(argv);
	if (err)
		warnx("upload to %s failed", uri);
out:
	free(src_dup);
	free(port);
	free(remote);
	return err;
}

static int scp_download(const char *uri, const char *dstpath)
{
	char *argv[12] = { "scp", "-o", "StrictHostKeyChecking=no", NULL };
	int i = 3, err = 1;
	char *remote = NULL, *port = NULL, *dst_dup = NULL;

	remote = ssh_uri_to_remote(uri, &port);
	if (!remote) {
		warnx("failed to parse URI: %s", uri);
		goto out;
	}

	if (port) {
		argv[i++] = "-P";
		argv[i++] = port;
	}

	argv[i++] = remote;
	dst_dup = strdup(dstpath);
	if (!dst_dup)
		goto out;
	argv[i++] = dst_dup;

	err = subprocess(argv);
	if (err)
		warnx("download of %s failed", uri);
out:
	free(dst_dup);
	free(port);
	free(remote);
	return err;
}

static int cat(const char *srcpath)
{
	char *argv[] = { "cat", NULL, NULL };
	int err;

	argv[1] = strdup(srcpath);
	if (!argv[1])
		return 1;

	err = subprocess(argv);
	if (err)
		warnx("failed writing to stdout, exit code %d", err);

	free(argv[1]);
	return err;
}

static int cp(const char *srcpath, const char *dstpath)
{
	char *argv[] =  {
		"cp", NULL, NULL, NULL,
	};
	int err = 1;

	argv[1] = strdup(srcpath);
	argv[2] = strdup(dstpath);
	if (!(argv[1] && argv[2]))
		goto out;

	err = subprocess(argv);
	if (err)
		warnx("failed to save %s, exit code %d", dstpath, err);
out:
	free(argv[2]);
	free(argv[1]);
	return err;
}

static int put(const char *srcpath, const char *dst,
	       const struct infix_ds *ds, const char *path)
{
	int err = 0;

	if (ds)
		err = sysrepo_import(ds, srcpath);
	else if (is_stdout(dst))
		err = cat(srcpath);
	else if (is_ssh_uri(dst))
		err = scp_upload(srcpath, dst);
	else if (is_uri(dst))
		err = curl_upload(srcpath, dst);

	if (err)
		return err;

	if (path) {
		err = cp(srcpath, path);
		if (!err)
			set_owner(path, getuser());
	}

	return 0;
}

static int get(const char *src, const struct infix_ds *ds, const char *path)
{
	int err = 0;

	if (ds)
		err = sysrepo_export(ds, path);
	else if (is_ssh_uri(src))
		err = scp_download(src, path);
	else if (is_uri(src))
		err = curl_download(src, path);

	return err;
}

static int resolve_src(const char **src, const struct infix_ds **ds, char **path, bool *rm)
{
	*src = infix_ds(*src, ds);

	if (*ds || is_uri(*src)) {
		*path = mktmp();
		if (!*path)
			return 1;

		*rm = true;
		return 0;
	} else {
		*path = cfg_adjust(*src, NULL, sanitize);
	}

	if (!*path) {
		warn("no such file %s", *src);
		return 1;
	}

	*rm = false;
	return 0;
}

static int resolve_dst(const char **dst, const struct infix_ds **ds, char **path)
{
	if (is_stdout(*dst) || is_uri(*dst))
		return 0;

	*dst = infix_ds(*dst, ds);

	if (*ds) {
		if (!(*ds)->rw) {
			warn("%s is not writable", (*ds)->name);
			return 1;
		}

		if (!(*ds)->path)
			return 0;

		*path = strdup((*ds)->path);
	} else {
		*path = cfg_adjust(*dst, NULL, sanitize);
	}

	if (!*path) {
		warn("no such file: %s", *dst);
		return 1;
	}

	if (!force && !*ds && !access(*path, F_OK) && !yorn("Overwrite existing file %s", *path)) {
		warnx("OK, aborting.");
		return 1;
	}

	return 0;
}

static int copy(const char *src, const char *dst)
{
	const struct infix_ds *srcds = NULL, *dstds = NULL;
	char *srcpath = NULL, *dstpath = NULL;
	char *dst_uri = NULL;
	bool rmsrc = false;
	mode_t oldmask;
	int err = 1;

	/* rw for user and group only */
	oldmask = umask(0006);

	if (dst && !strcmp(src, dst)) {
		warn("source and destination are the same, aborting.");
		goto err;
	}

	err = resolve_src(&src, &srcds, &srcpath, &rmsrc);
	if (err)
		goto err;

	/*
	 * When uploading to an SSH URI ending in '/', scp(1) would use the
	 * basename of the local (temp) file as the remote name.  Append a
	 * meaningful name instead: the datastore's on-disk filename, or the
	 * source file's own basename.
	 */
	if (dst && is_ssh_uri(dst) && dst[strlen(dst) - 1] == '/') {
		const char *bn, *slash;

		if (srcds && srcds->path) {
			slash = strrchr(srcds->path, '/');
			bn = slash ? slash + 1 : srcds->path;
		} else if (srcds) {
			bn = srcds->name;
		} else {
			slash = strrchr(srcpath, '/');
			bn = slash ? slash + 1 : srcpath;
		}

		if (asprintf(&dst_uri, "%s%s", dst, bn) < 0) {
			err = 1;
			goto err;
		}
		dst = dst_uri;
	}

	err = resolve_dst(&dst, &dstds, &dstpath);
	if (err)
		goto err;

	err = get(src, srcds, srcpath);
	if (err)
		goto err;

	err = put(srcpath, dst, dstds, dstpath);

err:
	/* If either src or dst came from sysrepo, close the session */
	sysrepo_session(NULL);

	if (rmsrc)
		rmtmp(srcpath);

	free(dst_uri);
	if (dstpath)
		free(dstpath);
	free(srcpath);

	sync();
	umask(oldmask);
	return err;
}

static int usage(int rc)
{
	printf("Usage: %s [OPTIONS] SRC [DST]\n"
	       "\n"
	       "Options:\n"
	       "  -d                 Enable debug mode, verbose output on stderr\n"
	       "  -f                 Force yes when copying to a file that exists already\n"
	       "  -h                 This help text\n"
	       "  -n                 Dry-run, validate configuration without applying\n"
	       "  -s                 Sanitize paths for CLI use (restrict path traversal)\n"
	       "  -t SEC             Timeout for the operation, or default %d sec\n"
	       "  -u USER            Username for remote commands, like scp\n"
	       "  -v                 Show version\n"
	       "  -x PATH            XPath to copy, default: all\n"
	       "\n"
	       "Files:\n"
	       "  SRC                JSON configuration file, or a datastore\n"
	       "  DST                Optiional file or datastore, except factory-config,\n"
	       "                     when omitted output goes to stdout\n"
	       "\n"
	       "Datastores (short forms possible):\n"
	       "  running-config     The running datastore, current active config\n"
	       "  startup-config     The non-volatile config used at startup\n"
	       "  factory-config     The device's factory default configuration\n"
	       "  operational-state  Operational status and state data"
	       "\n"
	       "Examples:\n"
	       "  %s operational -x /system-state/software/boot-order\n"
	       "\n", prognm, timeout, prognm);

	return rc;
}

static int usage_rpc(int rc)
{
	printf("Usage: %s [OPTIONS] <rpc-xpath> [key value ...]\n"
	       "\n"
	       "Execute a YANG RPC/action with NACM enforcement.\n"
	       "\n"
	       "Options:\n"
	       "  -d                 Enable debug mode, verbose output on stderr\n"
	       "  -h                 This help text\n"
	       "  -t SEC             Timeout for the operation, or default %d sec\n"
	       "  -v                 Show version\n"
	       "\n"
	       "Arguments:\n"
	       "  rpc-xpath          RPC XPath (e.g., /ietf-system:set-current-datetime)\n"
	       "  key value          Pairs of RPC argument names and values\n"
	       "                     Values can be comma-separated for lists/leaf-lists\n"
	       "\n"
	       "Examples:\n"
	       "  %s /ietf-system:set-current-datetime current-datetime \"2025-01-01T00:00:00Z\"\n"
	       "  %s /infix-system:set-boot-order boot-order primary boot-order secondary\n"
	       "  %s /infix-system:set-boot-order boot-order primary,secondary,net\n"
	       "\n", prognm, timeout, prognm, prognm, prognm);

	return rc;
}

/* Execute RPC from CLI arguments: xpath and key-value pairs */
static int rpc_exec(const char *rpc_xpath, int argc, char *argv[])
{
	sr_subscription_ctx_t *sub = NULL;
	sr_conn_ctx_t *conn = NULL;
	sr_session_ctx_t *sess = NULL;
	sr_val_t *input = NULL;
	sr_val_t *output = NULL;
	size_t icnt = 0, ocnt = 0;
	int rc = 1, err, i;

	dbg("Executing RPC %s with %d arguments", rpc_xpath, argc / 2);

	err = sysrepo_init(&conn, &sess, &sub);
	if (err != SR_ERR_OK)
		return 1;

	for (i = 0; i < argc - 1; i += 2) {
		const char *key = argv[i];
		const char *val = argv[i + 1];
		char *val_copy, *token, *saveptr;

		/* Check if value contains commas - split into multiple values */
		if (strchr(val, ',')) {
			val_copy = strdup(val);
			if (!val_copy) {
				warnx("Memory allocation failed");
				goto cleanup;
			}

			token = strtok_r(val_copy, ",", &saveptr);
			while (token) {
				sr_realloc_values(icnt, icnt + 1, &input);
				sr_val_build_xpath(&input[icnt], "%s/%s", rpc_xpath, key);
				sr_val_set_str_data(&input[icnt], SR_STRING_T, token);
				dbg("Adding RPC argument %zu: %s = %s", icnt, input[icnt].xpath, token);
				icnt++;
				token = strtok_r(NULL, ",", &saveptr);
			}
			free(val_copy);
		} else {
			/* Single value */
			sr_realloc_values(icnt, icnt + 1, &input);
			sr_val_build_xpath(&input[icnt], "%s/%s", rpc_xpath, key);
			sr_val_set_str_data(&input[icnt], SR_STRING_T, val);
			dbg("Adding RPC argument %zu: %s = %s", icnt, input[icnt].xpath, val);
			icnt++;
		}
	}

	dbg("Sending RPC %s (timeout: %d ms)", rpc_xpath, timeout * 1000);
	err = sr_rpc_send(sess, rpc_xpath, input, icnt, timeout * 1000, &output, &ocnt);
	if (err != SR_ERR_OK) {
		sysrepo_print_error(sess);
		warnx("RPC execution failed: %s", sr_strerror(err));
		goto cleanup;
	}

	/* Print output if any */
	for (i = 0; i < (int)ocnt; i++) {
		sr_print_val(&output[i]);
		puts("");
	}

	rc = 0;

cleanup:
	sr_free_values(input, icnt);
	sr_free_values(output, ocnt);
	if (sub)
		sr_nacm_destroy();
	if (sess)
		sr_session_stop(sess);
	if (conn)
		sr_disconnect(conn);

	return rc;
}

static int copy_main(int argc, char *argv[])
{
	const char *dst = "/dev/stdout";
	const char *src = NULL;
	int c;

	timeout = fgetint("/etc/default/confd", "=", "CONFD_TIMEOUT");

	while ((c = getopt(argc, argv, "dfhnst:u:vx:")) != EOF) {
		switch(c) {
		case 'd':
			debug = 1;
			break;
		case 'f':
			force = 1;
			break;
		case 'h':
			return usage(0);
		case 'n':
			dry_run = 1;
			break;
		case 's':
			sanitize = 1;
			break;
		case 't':
			timeout = atoi(optarg);
			break;
		case 'u':
			remote_user = optarg;
			break;
		case 'v':
			puts(PACKAGE_VERSION);
			return 0;
		case 'x':
			xpath = optarg;
			break;
		}
	}

	if (timeout < 0)
		timeout = 120;

	switch (argc - optind) {
	case 2:
		src = argv[optind++];
		dst = argv[optind++];
		break;
	case 1:
		src = argv[optind++];
		break;
	default:
		return usage(1);
	}

	return copy(src, dst);
}

static int rpc_main(int argc, char *argv[])
{
	int c;

	timeout = fgetint("/etc/default/confd", "=", "CONFD_TIMEOUT");

	while ((c = getopt(argc, argv, "dht:v")) != EOF) {
		switch(c) {
		case 'd':
			debug = 1;
			break;
		case 'h':
			return usage_rpc(0);
		case 't':
			timeout = atoi(optarg);
			break;
		case 'v':
			puts(PACKAGE_VERSION);
			return 0;
		}
	}

	if (timeout < 0)
		timeout = 120;

	/* Require at least RPC xpath */
	if (optind >= argc) {
		warnx("Missing RPC xpath");
		return usage_rpc(1);
	}

	/* Validate RPC xpath starts with '/' */
	if (argv[optind][0] != '/') {
		warnx("RPC xpath must start with '/'");
		return usage_rpc(1);
	}

	/* Validate argument count (must be key-value pairs) */
	argc -= optind + 1;
	if (argc % 2 != 0) {
		warnx("Arguments must be key-value pairs after RPC xpath");
		return usage_rpc(1);
	}

	return rpc_exec(argv[optind], argc, &argv[optind + 1]);
}

int main(int argc, char *argv[])
{
	prognm = basename(argv[0]);

	if (!strcmp(prognm, "rpc"))
		return rpc_main(argc, argv);

	return copy_main(argc, argv);
}
