/*
 * Copyright 2011-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
 * Copyright 2025 MNX Cloud, Inc.
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/types.h>
#include <sys/stdbool.h>
#include <sys/sysmacros.h>
#include <sys/bootvfs.h>
#include <sys/filep.h>
#include <sys/sunddi.h>
#include <sys/ccompile.h>
#include <sys/kobj.h>
#include <sys/queue.h>

/*
 * A cpio archive is just a sequence of files, each consisting of a header
 * (struct cpio_hdr) and the file contents.
 */

struct cpio_hdr {
	uint8_t		magic[6];
	uint8_t		dev[6];
	uint8_t		ino[6];
	uint8_t		mode[6];
	uint8_t		uid[6];
	uint8_t		gid[6];
	uint8_t		nlink[6];
	uint8_t		rdev[6];
	uint8_t		mtime[11];
	uint8_t		namesize[6];
	uint8_t		filesize[11];
	char		data[];
};

/*
 * This structure represents an open file.  The list of all open files is
 * rooted in the open_files global.
 */
struct cpio_file {
	/* pointers into the archive */
	const struct cpio_hdr *hdr;
	const char *path;		/* pointer into the archive */
	const void *data;		/* pointer into the archive */

	int fd;
	off_t off;
	struct bootstat stat;

	SLIST_ENTRY(cpio_file) next;
};

/*
 * in bootrd.c
 */
extern void *bkmem_alloc(size_t);
extern void bkmem_free(void *, size_t);

static void cpio_closeall(int flag);

static bool mounted;
static SLIST_HEAD(cpio_file_list, cpio_file)
    open_files = SLIST_HEAD_INITIALIZER(open_files);

/*
 * Returns the parsed number on success, or UINT64_MAX on error.  This is
 * ok because we will never deal with numbers that large in a cpio archive.
 */
static uint64_t
__get_uint64(const uint8_t *str, size_t len, const size_t output_size)
{
	uint64_t v;

	/* check that we can represent every number */
	if (len * 3 > output_size)
		return (UINT64_MAX);

	for (v = 0; len > 0; len--, str++) {
		const uint8_t c = *str;

		if ((c < '0') || (c > '7'))
			return (UINT64_MAX);

		v = (v * 8) + (c - '0');
	}

	return (v);
}

static bool
get_uint64(const uint8_t *str, size_t len, uint64_t *out)
{
	*out = __get_uint64(str, len, NBBY * sizeof (*out));
	return (*out != UINT64_MAX);
}

static bool
get_int64(const uint8_t *str, size_t len, int64_t *out)
{
	uint64_t tmp;

	tmp = __get_uint64(str, len, NBBY * sizeof (*out) - 1);

	*out = tmp;

	return (tmp != UINT64_MAX);
}

static bool
get_uint32(const uint8_t *str, size_t len, uint32_t *out)
{
	uint64_t tmp;

	tmp = __get_uint64(str, len, NBBY * sizeof (*out));

	*out = tmp;

	return (tmp != UINT64_MAX);
}

static bool
get_int32(const uint8_t *str, size_t len, int32_t *out)
{
	uint64_t tmp;

	tmp = __get_uint64(str, len, NBBY * sizeof (*out) - 1);

	*out = tmp;

	return (tmp != UINT64_MAX);
}

static void
add_open_file(struct cpio_file *file)
{
	SLIST_INSERT_HEAD(&open_files, file, next);
}

static void
remove_open_file(struct cpio_file *file)
{
	SLIST_REMOVE(&open_files, file, cpio_file, next);
}

static struct cpio_file *
find_open_file(int fd)
{
	struct cpio_file *file;

	if (fd < 0)
		return (NULL);

	SLIST_FOREACH(file, &open_files, next)
		if (file->fd == fd)
			return (file);

	return (NULL);
}

static const void *
read_ramdisk(size_t off, size_t len)
{
	const size_t first_block_offset = off % DEV_BSIZE;
	fileid_t tmpfile;

	/* return a dummy non-NULL pointer */
	if (len == 0)
		return ("");

	/* we have to read the stuff before the desired location as well */
	len += first_block_offset;

	tmpfile.fi_blocknum = off / DEV_BSIZE;
	tmpfile.fi_count = P2ROUNDUP_TYPED(len, DEV_BSIZE, size_t);
	tmpfile.fi_memp = NULL;

	if (diskread(&tmpfile) != 0)
		return (NULL);

	return (tmpfile.fi_memp + first_block_offset);
}

static bool
parse_stat(const struct cpio_hdr *hdr, struct bootstat *stat)
{
	if (!get_uint64(hdr->dev, sizeof (hdr->dev), &stat->st_dev))
		return (false);
	if (!get_uint64(hdr->ino, sizeof (hdr->ino), &stat->st_ino))
		return (false);
	if (!get_uint32(hdr->mode, sizeof (hdr->mode), &stat->st_mode))
		return (false);
	if (!get_int32(hdr->uid, sizeof (hdr->uid), &stat->st_uid))
		return (false);
	if (!get_int32(hdr->gid, sizeof (hdr->gid), &stat->st_gid))
		return (false);
	if (!get_uint32(hdr->nlink, sizeof (hdr->nlink), &stat->st_nlink))
		return (false);
	if (!get_uint64(hdr->rdev, sizeof (hdr->rdev), &stat->st_rdev))
		return (false);

	stat->st_mtim.tv_nsec = 0;
	if (!get_int64(hdr->mtime, sizeof (hdr->mtime), &stat->st_mtim.tv_sec))
		return (false);

	stat->st_atim = stat->st_mtim;
	stat->st_ctim = stat->st_mtim;

	if (!get_uint64(hdr->filesize, sizeof (hdr->filesize), &stat->st_size))
		return (false);

	stat->st_blksize = DEV_BSIZE;
	stat->st_blocks = P2ROUNDUP(stat->st_size, DEV_BSIZE);

	return (true);
}

static int
check_archive_hdr(const struct cpio_hdr *hdr)
{
	if ((hdr->magic[0] != '0') || (hdr->magic[1] != '7') ||
	    (hdr->magic[2] != '0') || (hdr->magic[3] != '7') ||
	    (hdr->magic[4] != '0') || (hdr->magic[5] != '7'))
		return (-1);
	return (0);
}

/*
 * Check if specified header is for a file with a specific path.  If so,
 * fill in the file struct and return 0.  If not, return number of bytes to
 * skip over to get to the next header.  If an error occurs, -1 is returned.
 * If end of archive is reached, return -2 instead.
 */
static ssize_t
scan_archive_hdr(const struct cpio_hdr *hdr, size_t off,
    struct cpio_file *file, const char *wanted_path)
{
	struct bootstat stat;
	uint32_t namesize;
	uint64_t filesize;
	const char *path;
	const void *data;

	if (check_archive_hdr(hdr))
		return (-1);

	if (!get_uint32(hdr->namesize, sizeof (hdr->namesize), &namesize))
		return (-1);
	if (!get_uint64(hdr->filesize, sizeof (hdr->filesize), &filesize))
		return (-1);

	/*
	 * We have the two sizes, let's try to read the name and file
	 * contents to make sure they are part of the ramdisk.
	 */

	off += offsetof(struct cpio_hdr, data[0]);
	path = read_ramdisk(off, namesize);
	data = read_ramdisk(off + namesize, filesize);

	/* either read failing is fatal */
	if (path == NULL || data == NULL)
		return (-1);

	if (strcmp(path, "TRAILER!!!") == 0)
		return (-2);

	if (strcmp(path, wanted_path) != 0)
		return (offsetof(struct cpio_hdr, data[namesize + filesize]));

	/*
	 * This is the file we want!
	 */

	if (!parse_stat(hdr, &stat))
		return (-1);

	file->hdr = hdr;
	file->path = path;
	file->data = data;
	file->stat = stat;

	return (0);
}

static int
find_filename(char *path, struct cpio_file *file)
{
	size_t off;

	/*
	 * The paths in the cpio boot archive omit the leading '/'.  So,
	 * skip checking for it.  If the searched for path does not include
	 * the leading path (it's a relative path), fail the lookup.
	 */
	if (path[0] != '/')
		return (-1);

	path++;

	/* now scan the archive for the relevant file */

	off = 0;

	for (;;) {
		const struct cpio_hdr *hdr;
		ssize_t size;

		hdr = (struct cpio_hdr *)read_ramdisk(off,
		    sizeof (struct cpio_hdr));
		if (hdr == NULL)
			return (-1);

		size = scan_archive_hdr(hdr, off, file, path);
		if (size <= 0)
			return (size);

		off += size;
	}
}

static int
bcpio_mountroot(char *str __unused)
{
	const struct cpio_hdr *hdr;

	if (mounted)
		return (-1);

	hdr = (struct cpio_hdr *)read_ramdisk(0, sizeof (struct cpio_hdr));
	if (hdr == NULL)
		return (-1);

	if (check_archive_hdr(hdr))
		return (-1);

	mounted = true;

	return (0);
}

static int
bcpio_unmountroot(void)
{
	if (!mounted)
		return (-1);

	mounted = false;

	return (0);
}

static int
bcpio_open(char *path, int flags __unused)
{
	static int filedes = 1;
	struct cpio_file temp_file;
	struct cpio_file *file;

	if (find_filename(path, &temp_file) != 0)
		return (-1);

	file = bkmem_alloc(sizeof (struct cpio_file));
	file->hdr = temp_file.hdr;
	file->path = temp_file.path;
	file->data = temp_file.data;
	file->stat = temp_file.stat;
	file->fd = filedes++;
	file->off = 0;

	add_open_file(file);

	return (file->fd);
}

static int
bcpio_close(int fd)
{
	struct cpio_file *file;

	file = find_open_file(fd);
	if (file == NULL)
		return (-1);

	remove_open_file(file);

	bkmem_free(file, sizeof (struct cpio_file));

	return (0);
}

static void
bcpio_closeall(int flag __unused)
{
	struct cpio_file *file;

	while (!SLIST_EMPTY(&open_files)) {
		file = SLIST_FIRST(&open_files);

		if (bcpio_close(file->fd) != 0) {
			kobj_printf("closeall invoked close(%d) failed\n",
			    file->fd);
		}
	}
}

static ssize_t
bcpio_read(int fd, caddr_t buf, size_t size)
{
	struct cpio_file *file;

	file = find_open_file(fd);
	if (file == NULL)
		return (-1);

	if (size == 0)
		return (0);

	if (file->off + size > file->stat.st_size)
		size = file->stat.st_size - file->off;

	bcopy((void *)((uintptr_t)file->data + file->off), buf, size);

	file->off += size;

	return (size);
}

static off_t
bcpio_lseek(int fd, off_t addr, int whence)
{
	struct cpio_file *file;

	file = find_open_file(fd);
	if (file == NULL)
		return (-1);

	switch (whence) {
		case SEEK_CUR:
			file->off += addr;
			break;
		case SEEK_SET:
			file->off = addr;
			break;
		case SEEK_END:
			file->off = file->stat.st_size;
			break;
		default:
			kobj_printf("lseek(): invalid whence value %d\n",
			    whence);
			return (-1);
	}

	return (0);
}

static int
bcpio_fstat(int fd, struct bootstat *buf)
{
	const struct cpio_file *file;

	file = find_open_file(fd);
	if (file == NULL)
		return (-1);

	*buf = file->stat;

	return (0);
}

struct boot_fs_ops bcpio_ops = {
	.fsw_name		= "boot_cpio",
	.fsw_mountroot		= bcpio_mountroot,
	.fsw_unmountroot	= bcpio_unmountroot,
	.fsw_open		= bcpio_open,
	.fsw_close		= bcpio_close,
	.fsw_closeall		= bcpio_closeall,
	.fsw_read		= bcpio_read,
	.fsw_lseek		= bcpio_lseek,
	.fsw_fstat		= bcpio_fstat,
};
