// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (C) 2011 Oracle.  All rights reserved.
 */
#define _XOPEN_SOURCE 500
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <ctype.h>
#include <errno.h>
#include <limits.h>

#ifndef SEEK_DATA
#define SEEK_DATA	3
#define SEEK_HOLE	4
#endif

#define BUF_SIZE	4096
#ifndef MIN
#define MIN(a,b) ((a)<(b)?(a):(b))
#endif

static void
error(const char *fmt, ...)
{
	char buf[256];
	va_list args;
	va_start(args, fmt);
	vsprintf(buf, fmt, args);
	va_end(args);

	fprintf(stderr, "ERROR: [%s:%d] %s:%s\n", __func__, __LINE__,
		buf, strerror(errno));
}

static size_t
full_write(int fd, const void *buf, size_t count)
{
	size_t total = 0;
	const char *ptr = (const char *) buf;

	while (count > 0) {
		ssize_t n = write(fd, ptr, count);
		if (n < 0) {
			if (errno == EINTR)
				continue;
			error("failed as %s", strerror(errno));
			break;
		}

		if (n == 0) {
			error("%zu bytes transferred. Aborting.",
			       total);
			break;
		}

		total += n;
		ptr += n;
		count -= n;
	}

	return total;
}

/*
 * Copy a data extent from source file to dest file.
 * @data_off: data offset
 * @hole_off: hole offset
 * The length of this extent is (hole_off - data_off).
 */
static int
do_extent_copy(int src_fd, int dest_fd, off_t data_off, off_t hole_off)
{
	uint64_t len = (uint64_t)(hole_off - data_off);
	char buf[BUF_SIZE];
	int ret;

	/* Seek to data_off for data reading */
	ret = lseek(src_fd, data_off, SEEK_SET);
	if (ret < 0) {
		error("seek source file to %llu failed as %s",
		       (uint64_t)data_off, strerror(errno));
		return ret;
	}

	/* Seek to data_off for data writing, make holes as well */
	ret = lseek(dest_fd, data_off, SEEK_SET);
	if (ret < 0) {
		error("seek dest file to %llu failed as %s",
		       (uint64_t)data_off, strerror(errno));
		return ret;
	}

	while (len > 0) {
		ssize_t nr_read = read(src_fd, buf, BUF_SIZE);
		if (nr_read < 0) {
			if (errno == EINTR)
				continue;
			error("read source file extent failed as %s",
			      strerror(errno));
			ret = -1;
			break;
		}

		if (nr_read == 0) {
			error("reached EOF");
			break;
		}

		if (full_write(dest_fd, buf, nr_read) != nr_read) {
			error("write data to dest file failed as %s",
			       strerror(errno));
			ret = -1;
			break;
		}

		len -= nr_read;
	}

	return ret;
}

/*
 * If lseek(2) failed and the errno is set to ENXIO, for
 * SEEK_DATA there are no more data regions past the supplied
 * offset.  For SEEK_HOLE, there are no more holes past the
 * supplied offset.  Set scan->hit_final_extent to true for
 * either case.
 */
static int
copy_extents(int src_fd, int dest_fd, off_t src_total_size)
{
	int ret = 0;
	off_t seek_start = 0;
	off_t dest_pos = 0;
	off_t data_pos, hole_pos;

	do {
		data_pos = lseek(src_fd, seek_start, SEEK_DATA);
		if (data_pos < 0) {
			if (errno == ENXIO)
				ret = 0;
			else {
				error("SEEK_DATA failed due to %s",
				       strerror(errno));
				ret = -1;
			}
			break;
		}

		hole_pos = lseek(src_fd, data_pos, SEEK_HOLE);
		if (hole_pos < 0) {
			if (errno == ENXIO)
				ret = 0;
			else {
				error("SEEK_HOLE failed due to %s\n",
				       strerror(errno));
				ret = -1;
			}
			break;
		}

		/* do extent copy */
		ret = do_extent_copy(src_fd, dest_fd, data_pos, hole_pos);
		if (ret < 0) {
			error("copy extent failed");
			break;
		}

		dest_pos += (hole_pos - data_pos);
		seek_start = hole_pos;
	} while (seek_start < src_total_size);

	if (dest_pos < src_total_size) {
		ret = ftruncate(dest_fd, src_total_size);
		if (ret < 0) {
			error("truncate dest file to %lld bytes failed as %s",
			      (long long)src_total_size, strerror(errno));
		}
	}

	return ret;
}

int
main(int argc, char **argv)
{
	int ret = 0;
	int src_fd;
	int dest_fd;
	struct stat st;
	size_t src_total_size;

	if (argc != 3) {
		fprintf(stdout, "Usage: %s source dest\n", argv[0]);
		return 1;
	}

	src_fd = open(argv[1], O_RDONLY, 0644);
	if (src_fd < 0) {
		error("create %s failed", argv[1]);
		return -1;
	}

	dest_fd = open(argv[2], O_RDWR|O_CREAT|O_EXCL, 0644);
	if (dest_fd < 0) {
		error("create %s failed", argv[2]);
		ret = -errno;
		goto close_src_fd;
	}

	ret = fstat(src_fd, &st);
	if (ret < 0) {
		error("get file %s staticis failed", argv[1]);
		ret = -errno;
		goto close_dest_fd;
	}

	src_total_size = st.st_size;
	ret = copy_extents(src_fd, dest_fd, src_total_size);
	if (ret < 0)
		error("extents copy failed");

close_dest_fd:
	close(dest_fd);
close_src_fd:
	close(src_fd);

	return ret;
}
