/*
 * The Qubes OS Project, http://www.qubes-os.org
 *
 * Copyright (C) 2010  Rafal Wojtczuk  <rafal@invisiblethingslab.com>
 *
 * 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 <linux/module.h>
#include <linux/version.h>
#include <linux/proc_fs.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/io.h>
#ifndef FOREIGN_FRAME_BIT
#include <xen/page.h>
#endif
#include <linux/highmem.h>

/* copy of /usr/include/u2mfn-kernel.h, to reduce requirements */
#include <linux/ioctl.h>
#define U2MFN_MAGIC 0xf5
#define U2MFN_GET_MFN_FOR_PAGE _IOW(U2MFN_MAGIC, 1, int)
#define U2MFN_GET_LAST_MFN _IO(U2MFN_MAGIC, 2)


#if LINUX_VERSION_CODE < KERNEL_VERSION(3,0,0)
static inline unsigned long virt_to_phys(volatile void *address)
{
	return __pa((unsigned long) address);
}
#endif

#ifdef virt_to_mfn
#define VIRT_TO_MFN virt_to_mfn
#else
extern unsigned long *phys_to_machine_mapping;
static inline unsigned long VIRT_TO_MFN(void *addr)
{
	return phys_to_machine_mapping[virt_to_phys(addr) >> PAGE_SHIFT] & ~FOREIGN_FRAME_BIT;
}
#endif

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,3,0)
static int u2mfn_get_mfn(pte_t *pte, unsigned long addr, void *data) {
    *((unsigned long *) data) = pfn_to_mfn(pte_pfn(*pte));
    return 0;
}
#else
static int u2mfn_get_mfn(pte_t *pte, pgtable_t token, unsigned long addr, void *data) {
    *((unsigned long *) data) = pfn_to_mfn(pte_pfn(*pte));
    return 0;
}
#endif

/// User virtual address to mfn translator
/**
    \param cmd ignored
    \param data the user-specified address
    \return mfn corresponding to "data" argument, or -1 on error
*/
static long u2mfn_ioctl(struct file *f, unsigned int cmd,
		       unsigned long data)
{
	long ret;
	unsigned long mfn;

	if (_IOC_TYPE(cmd) != U2MFN_MAGIC) {
		printk("Qubes u2mfn: wrong IOCTL magic");
		return -ENOTTY;
	}

	switch (cmd) {
	case U2MFN_GET_MFN_FOR_PAGE:
		ret = apply_to_page_range(current->mm, data, PAGE_SIZE, u2mfn_get_mfn, &mfn);

		if (ret < 0 || mfn == INVALID_P2M_ENTRY) {
			printk("U2MFN_GET_MFN_FOR_PAGE: failed to get mfn, "
			       "addr=0x%lx ret=0x%lx\n", data, ret);
			return -1;
		}

		ret = mfn;
		break;

	case U2MFN_GET_LAST_MFN:
		if (f->private_data)
			ret = VIRT_TO_MFN(f->private_data);
		else
			ret = 0;
		break;

	default:
		printk("Qubes u2mfn: wrong ioctl passed!\n");
		return -ENOTTY;
	}


	return ret;
}

static int u2mfn_mmap(struct file *f, struct vm_area_struct *vma)
{
	int ret;
	char *kbuf;
	long length = vma->vm_end - vma->vm_start;
	printk("u2mfn_mmap: entering, private=%p\n", f->private_data);
	if (f->private_data)
		return -EBUSY;
	if (length != PAGE_SIZE)
		return -EINVAL;
	kbuf = (char *) __get_free_page(GFP_KERNEL);
	if (!kbuf)
		return -ENOMEM;

	f->private_data = kbuf;

	ret = remap_pfn_range(vma, vma->vm_start,
			      virt_to_phys(kbuf) >> PAGE_SHIFT,
			      length, vma->vm_page_prot);

	printk("u2mfn_mmap: calling remap return %d\n", ret);
	if (ret)
		return ret;


	return 0;
}

static int u2mfn_release(struct inode *i, struct file *f)
{
	printk("u2mfn_release, priv=%p\n", f->private_data);
	if (f->private_data)
		__free_page(f->private_data);
	f->private_data = NULL;
	return 0;
}

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,6,0)
static struct proc_ops u2mfn_fops = {
	.proc_ioctl = u2mfn_ioctl,
	.proc_mmap = u2mfn_mmap,
	.proc_release = u2mfn_release
};
#else
static struct file_operations u2mfn_fops = {
	.unlocked_ioctl = u2mfn_ioctl,
	.mmap = u2mfn_mmap,
	.release = u2mfn_release
};
#endif

/// u2mfn module registration
/**
    tries to register "/proc/u2mfn" pseudofile
*/
static int u2mfn_init(void)
{
	struct proc_dir_entry *u2mfn_node =
	    proc_create_data("u2mfn", 0666, NULL,
			     &u2mfn_fops, 0);
	if (!u2mfn_node)
		return -1;
	return 0;
}

static void u2mfn_exit(void)
{
	remove_proc_entry("u2mfn", 0);
}

module_init(u2mfn_init);
module_exit(u2mfn_exit);
MODULE_LICENSE("GPL");
MODULE_VERSION("5.0.0");
