/*
 * utils - miscellaneous I/O utilities for cryptsetup
 *
 * Copyright (C) 2004 Jana Saout <jana@saout.de>
 * Copyright (C) 2004-2007 Clemens Fruhwirth <clemens@endorphin.org>
 * Copyright (C) 2009-2021 Red Hat, Inc. All rights reserved.
 * Copyright (C) 2009-2021 Milan Broz
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/types.h>

#include "utils_io.h"

static ssize_t _read_buffer(int fd, void *buf, size_t length, volatile int *quit)
{
	size_t read_size = 0;
	ssize_t r;

	if (fd < 0 || !buf)
		return -EINVAL;

	do {
		r = read(fd, buf, length - read_size);
		if (r == -1 && errno != EINTR)
			return r;
		if (r > 0) {
			read_size += (size_t)r;
			buf = (uint8_t*)buf + r;
		}
		if (r == 0 || (quit && *quit))
			return (ssize_t)read_size;
	} while (read_size != length);

	return (ssize_t)length;
}

ssize_t read_buffer(int fd, void *buf, size_t length)
{
	return _read_buffer(fd, buf, length, NULL);
}

ssize_t read_buffer_intr(int fd, void *buf, size_t length, volatile int *quit)
{
	return _read_buffer(fd, buf, length, quit);
}

static ssize_t _write_buffer(int fd, const void *buf, size_t length, volatile int *quit)
{
	size_t write_size = 0;
	ssize_t w;

	if (fd < 0 || !buf || !length)
		return -EINVAL;

	do {
		w = write(fd, buf, length - write_size);
		if (w < 0 && errno != EINTR)
			return w;
		if (w > 0) {
			write_size += (size_t) w;
			buf = (const uint8_t*)buf + w;
		}
		if (w == 0 || (quit && *quit))
			return (ssize_t)write_size;
	} while (write_size != length);

	return (ssize_t)write_size;
}

ssize_t write_buffer(int fd, const void *buf, size_t length)
{
	return _write_buffer(fd, buf, length, NULL);
}

ssize_t write_buffer_intr(int fd, const void *buf, size_t length, volatile int *quit)
{
	return _write_buffer(fd, buf, length, quit);
}

ssize_t write_blockwise(int fd, size_t bsize, size_t alignment,
			void *orig_buf, size_t length)
{
	void *hangover_buf = NULL, *buf = NULL;
	size_t hangover, solid;
	ssize_t r, ret = -1;

	if (fd == -1 || !orig_buf || !bsize || !alignment)
		return -1;

	hangover = length % bsize;
	solid = length - hangover;

	if ((size_t)orig_buf & (alignment - 1)) {
		if (posix_memalign(&buf, alignment, length))
			return -1;
		memcpy(buf, orig_buf, length);
	} else
		buf = orig_buf;

	if (solid) {
		r = write_buffer(fd, buf, solid);
		if (r < 0 || r != (ssize_t)solid)
			goto out;
	}

	if (hangover) {
		if (posix_memalign(&hangover_buf, alignment, bsize))
			goto out;
		memset(hangover_buf, 0, bsize);

		r = read_buffer(fd, hangover_buf, bsize);
		if (r < 0)
			goto out;

		if (lseek(fd, -(off_t)r, SEEK_CUR) < 0)
			goto out;

		memcpy(hangover_buf, (char*)buf + solid, hangover);

		r = write_buffer(fd, hangover_buf, bsize);
		if (r < 0 || r < (ssize_t)hangover)
			goto out;
	}
	ret = length;
out:
	free(hangover_buf);
	if (buf != orig_buf)
		free(buf);
	return ret;
}

ssize_t read_blockwise(int fd, size_t bsize, size_t alignment,
		       void *orig_buf, size_t length)
{
	void *hangover_buf = NULL, *buf = NULL;
	size_t hangover, solid;
	ssize_t r, ret = -1;

	if (fd == -1 || !orig_buf || !bsize || !alignment)
		return -1;

	hangover = length % bsize;
	solid = length - hangover;

	if ((size_t)orig_buf & (alignment - 1)) {
		if (posix_memalign(&buf, alignment, length))
			return -1;
	} else
		buf = orig_buf;

	r = read_buffer(fd, buf, solid);
	if (r < 0 || r != (ssize_t)solid)
		goto out;

	if (hangover) {
		if (posix_memalign(&hangover_buf, alignment, bsize))
			goto out;
		r = read_buffer(fd, hangover_buf, bsize);
		if (r <  0 || r < (ssize_t)hangover)
			goto out;

		memcpy((char *)buf + solid, hangover_buf, hangover);
	}
	ret = length;
out:
	free(hangover_buf);
	if (buf != orig_buf) {
		if (ret != -1)
			memcpy(orig_buf, buf, length);
		free(buf);
	}
	return ret;
}

/*
 * Combines llseek with blockwise write. write_blockwise can already deal with short writes
 * but we also need a function to deal with short writes at the start. But this information
 * is implicitly included in the read/write offset, which can not be set to non-aligned
 * boundaries. Hence, we combine llseek with write.
 */
ssize_t write_lseek_blockwise(int fd, size_t bsize, size_t alignment,
			      void *buf, size_t length, off_t offset)
{
	void *frontPadBuf = NULL;
	size_t frontHang, innerCount = 0;
	ssize_t r, ret = -1;

	if (fd == -1 || !buf || !bsize || !alignment)
		return -1;

	if (offset < 0)
		offset = lseek(fd, offset, SEEK_END);

	if (offset < 0)
		return -1;

	frontHang = offset % bsize;

	if (lseek(fd, offset - frontHang, SEEK_SET) < 0)
		return -1;

	if (frontHang && length) {
		if (posix_memalign(&frontPadBuf, alignment, bsize))
			return -1;

		innerCount = bsize - frontHang;
		if (innerCount > length)
			innerCount = length;

		r = read_buffer(fd, frontPadBuf, bsize);
		if (r < 0 || r < (ssize_t)(frontHang + innerCount))
			goto out;

		memcpy((char*)frontPadBuf + frontHang, buf, innerCount);

		if (lseek(fd, offset - frontHang, SEEK_SET) < 0)
			goto out;

		r = write_buffer(fd, frontPadBuf, bsize);
		if (r < 0 || r != (ssize_t)bsize)
			goto out;

		buf = (char*)buf + innerCount;
		length -= innerCount;
	}

	ret = length ? write_blockwise(fd, bsize, alignment, buf, length) : 0;
	if (ret >= 0)
		ret += innerCount;
out:
	free(frontPadBuf);
	return ret;
}

ssize_t read_lseek_blockwise(int fd, size_t bsize, size_t alignment,
			     void *buf, size_t length, off_t offset)
{
	void *frontPadBuf = NULL;
	size_t frontHang, innerCount = 0;
	ssize_t r, ret = -1;

	if (fd == -1 || !buf || bsize <= 0)
		return -1;

	if (offset < 0)
		offset = lseek(fd, offset, SEEK_END);

	if (offset < 0)
		return -1;

	frontHang = offset % bsize;

	if (lseek(fd, offset - frontHang, SEEK_SET) < 0)
		return -1;

	if (frontHang && length) {
		if (posix_memalign(&frontPadBuf, alignment, bsize))
			return -1;

		innerCount = bsize - frontHang;
		if (innerCount > length)
			innerCount = length;

		r = read_buffer(fd, frontPadBuf, bsize);
		if (r < 0 || r < (ssize_t)(frontHang + innerCount))
			goto out;

		memcpy(buf, (char*)frontPadBuf + frontHang, innerCount);

		buf = (char*)buf + innerCount;
		length -= innerCount;
	}

	ret = read_blockwise(fd, bsize, alignment, buf, length);
	if (ret >= 0)
		ret += innerCount;
out:
	free(frontPadBuf);
	return ret;
}
