// Copyright 2023 Linaro Ltd. All Rights Reserved.
//          Viresh Kumar <viresh.kumar@linaro.org>
//
// Xen specific memory mapping implementations
//
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause

//! Helper structure for working with mmap'ed memory regions on Xen.

use bitflags::bitflags;
use libc::{c_int, c_void, MAP_SHARED, _SC_PAGESIZE};
use std::{io, mem::size_of, os::raw::c_ulong, os::unix::io::AsRawFd, ptr::null_mut, result};

use vmm_sys_util::{
    fam::{Error as FamError, FamStruct, FamStructWrapper},
    generate_fam_struct_impl,
    ioctl::{ioctl_expr, _IOC_NONE},
};

// Use a dummy ioctl implementation for tests instead.
#[cfg(not(test))]
use vmm_sys_util::ioctl::ioctl_with_ref;

#[cfg(test)]
use tests::ioctl_with_ref;

use crate::bitmap::{Bitmap, BS};
use crate::guest_memory::{FileOffset, GuestAddress};
use crate::mmap::{check_file_offset, NewBitmap};
use crate::volatile_memory::{self, VolatileMemory, VolatileSlice};

/// Error conditions that may arise when creating a new `MmapRegion` object.
#[derive(Debug, thiserror::Error)]
pub enum Error {
    /// The specified file offset and length cause overflow when added.
    #[error("The specified file offset and length cause overflow when added")]
    InvalidOffsetLength,
    /// The forbidden `MAP_FIXED` flag was specified.
    #[error("The forbidden `MAP_FIXED` flag was specified")]
    MapFixed,
    /// A mapping with offset + length > EOF was attempted.
    #[error("The specified file offset and length is greater then file length")]
    MappingPastEof,
    /// The `mmap` call returned an error.
    #[error("{0}")]
    Mmap(io::Error),
    /// Seeking the end of the file returned an error.
    #[error("Error seeking the end of the file: {0}")]
    SeekEnd(io::Error),
    /// Seeking the start of the file returned an error.
    #[error("Error seeking the start of the file: {0}")]
    SeekStart(io::Error),
    /// Invalid file offset.
    #[error("Invalid file offset")]
    InvalidFileOffset,
    /// Memory mapped in advance.
    #[error("Memory mapped in advance")]
    MappedInAdvance,
    /// Invalid Xen mmap flags.
    #[error("Invalid Xen Mmap flags: {0:x}")]
    MmapFlags(u32),
    /// Fam error.
    #[error("Fam error: {0}")]
    Fam(FamError),
    /// Unexpected error.
    #[error("Unexpected error")]
    UnexpectedError,
}

type Result<T> = result::Result<T, Error>;

/// `MmapRange` represents a range of arguments required to create Mmap regions.
#[derive(Clone, Debug)]
pub struct MmapRange {
    size: usize,
    file_offset: Option<FileOffset>,
    prot: Option<i32>,
    flags: Option<i32>,
    hugetlbfs: Option<bool>,
    addr: GuestAddress,
    mmap_flags: u32,
    mmap_data: u32,
}

impl MmapRange {
    /// Creates instance of the range with multiple arguments.
    pub fn new(
        size: usize,
        file_offset: Option<FileOffset>,
        addr: GuestAddress,
        mmap_flags: u32,
        mmap_data: u32,
    ) -> Self {
        Self {
            size,
            file_offset,
            prot: None,
            flags: None,
            hugetlbfs: None,
            addr,
            mmap_flags,
            mmap_data,
        }
    }

    /// Creates instance of the range for `MmapXenFlags::UNIX` type mapping.
    pub fn new_unix(size: usize, file_offset: Option<FileOffset>, addr: GuestAddress) -> Self {
        let flags = Some(match file_offset {
            Some(_) => libc::MAP_NORESERVE | libc::MAP_SHARED,
            None => libc::MAP_ANONYMOUS | libc::MAP_PRIVATE,
        });

        Self {
            size,
            file_offset,
            prot: None,
            flags,
            hugetlbfs: None,
            addr,
            mmap_flags: MmapXenFlags::UNIX.bits(),
            mmap_data: 0,
        }
    }

    /// Set the prot of the range.
    pub fn set_prot(&mut self, prot: i32) {
        self.prot = Some(prot)
    }

    /// Set the flags of the range.
    pub fn set_flags(&mut self, flags: i32) {
        self.flags = Some(flags)
    }

    /// Set the hugetlbfs of the range.
    pub fn set_hugetlbfs(&mut self, hugetlbfs: bool) {
        self.hugetlbfs = Some(hugetlbfs)
    }
}

/// Helper structure for working with mmaped memory regions with Xen.
///
/// The structure is used for accessing the guest's physical memory by mmapping it into
/// the current process.
///
/// # Limitations
/// When running a 64-bit virtual machine on a 32-bit hypervisor, only part of the guest's
/// physical memory may be mapped into the current process due to the limited virtual address
/// space size of the process.
#[derive(Debug)]
pub struct MmapRegion<B = ()> {
    bitmap: B,
    size: usize,
    prot: i32,
    flags: i32,
    file_offset: Option<FileOffset>,
    hugetlbfs: Option<bool>,
    mmap: MmapXen,
}

// SAFETY: Send and Sync aren't automatically inherited for the raw address pointer.
// Accessing that pointer is only done through the stateless interface which
// allows the object to be shared by multiple threads without a decrease in
// safety.
unsafe impl<B: Send> Send for MmapRegion<B> {}
// SAFETY: See comment above.
unsafe impl<B: Sync> Sync for MmapRegion<B> {}

impl<B: NewBitmap> MmapRegion<B> {
    /// Creates a shared anonymous mapping of `size` bytes.
    ///
    /// # Arguments
    /// * `range` - An instance of type `MmapRange`.
    ///
    /// # Examples
    /// * Write a slice at guest address 0x1200 with Xen's Grant mapping.
    ///
    /// ```
    /// use std::fs::File;
    /// use std::path::Path;
    /// use vm_memory::{
    ///     Bytes, FileOffset, GuestAddress, GuestMemoryMmap, GuestRegionMmap, MmapRange, MmapRegion,
    ///     MmapXenFlags,
    /// };
    /// # use vmm_sys_util::tempfile::TempFile;
    ///
    /// let addr = GuestAddress(0x1000);
    /// # if false {
    /// let file = Some(FileOffset::new(
    ///     File::open(Path::new("/dev/xen/gntdev")).expect("Could not open file"),
    ///     0,
    /// ));
    ///
    /// let range = MmapRange::new(0x400, file, addr, MmapXenFlags::GRANT.bits(), 0);
    /// # }
    /// # // We need a UNIX mapping for tests to succeed.
    /// # let range = MmapRange::new_unix(0x400, None, addr);
    ///
    /// let r = GuestRegionMmap::new(
    ///     MmapRegion::<()>::from_range(range).expect("Could not create mmap region"),
    ///     addr,
    /// )
    /// .expect("Could not create guest region");
    ///
    /// let mut gm = GuestMemoryMmap::from_regions(vec![r]).expect("Could not create guest memory");
    /// let res = gm
    ///     .write(&[1, 2, 3, 4, 5], GuestAddress(0x1200))
    ///     .expect("Could not write to guest memory");
    /// assert_eq!(5, res);
    /// ```
    ///
    /// * Write a slice at guest address 0x1200 with Xen's Foreign mapping.
    ///
    /// ```
    /// use std::fs::File;
    /// use std::path::Path;
    /// use vm_memory::{
    ///     Bytes, FileOffset, GuestAddress, GuestMemoryMmap, GuestRegionMmap, MmapRange, MmapRegion,
    ///     MmapXenFlags,
    /// };
    /// # use vmm_sys_util::tempfile::TempFile;
    ///
    /// let addr = GuestAddress(0x1000);
    /// # if false {
    /// let file = Some(FileOffset::new(
    ///     File::open(Path::new("/dev/xen/privcmd")).expect("Could not open file"),
    ///     0,
    /// ));
    ///
    /// let range = MmapRange::new(0x400, file, addr, MmapXenFlags::FOREIGN.bits(), 0);
    /// # }
    /// # // We need a UNIX mapping for tests to succeed.
    /// # let range = MmapRange::new_unix(0x400, None, addr);
    ///
    /// let r = GuestRegionMmap::new(
    ///     MmapRegion::<()>::from_range(range).expect("Could not create mmap region"),
    ///     addr,
    /// )
    /// .expect("Could not create guest region");
    ///
    /// let mut gm = GuestMemoryMmap::from_regions(vec![r]).expect("Could not create guest memory");
    /// let res = gm
    ///     .write(&[1, 2, 3, 4, 5], GuestAddress(0x1200))
    ///     .expect("Could not write to guest memory");
    /// assert_eq!(5, res);
    /// ```
    pub fn from_range(mut range: MmapRange) -> Result<Self> {
        if range.prot.is_none() {
            range.prot = Some(libc::PROT_READ | libc::PROT_WRITE);
        }

        match range.flags {
            Some(flags) => {
                if flags & libc::MAP_FIXED != 0 {
                    // Forbid MAP_FIXED, as it doesn't make sense in this context, and is pretty dangerous
                    // in general.
                    return Err(Error::MapFixed);
                }
            }
            None => range.flags = Some(libc::MAP_NORESERVE | libc::MAP_SHARED),
        }

        let mmap = MmapXen::new(&range)?;

        Ok(MmapRegion {
            bitmap: B::with_len(range.size),
            size: range.size,
            prot: range.prot.ok_or(Error::UnexpectedError)?,
            flags: range.flags.ok_or(Error::UnexpectedError)?,
            file_offset: range.file_offset,
            hugetlbfs: range.hugetlbfs,
            mmap,
        })
    }
}

impl<B: Bitmap> MmapRegion<B> {
    /// Returns a pointer to the beginning of the memory region. Mutable accesses performed
    /// using the resulting pointer are not automatically accounted for by the dirty bitmap
    /// tracking functionality.
    ///
    /// Should only be used for passing this region to ioctls for setting guest memory.
    pub fn as_ptr(&self) -> *mut u8 {
        self.mmap.addr()
    }

    /// Returns the size of this region.
    pub fn size(&self) -> usize {
        self.size
    }

    /// Returns information regarding the offset into the file backing this region (if any).
    pub fn file_offset(&self) -> Option<&FileOffset> {
        self.file_offset.as_ref()
    }

    /// Returns the value of the `prot` parameter passed to `mmap` when mapping this region.
    pub fn prot(&self) -> i32 {
        self.prot
    }

    /// Returns the value of the `flags` parameter passed to `mmap` when mapping this region.
    pub fn flags(&self) -> i32 {
        self.flags
    }

    /// Checks whether this region and `other` are backed by overlapping
    /// [`FileOffset`](struct.FileOffset.html) objects.
    ///
    /// This is mostly a sanity check available for convenience, as different file descriptors
    /// can alias the same file.
    pub fn fds_overlap<T: Bitmap>(&self, other: &MmapRegion<T>) -> bool {
        if let Some(f_off1) = self.file_offset() {
            if let Some(f_off2) = other.file_offset() {
                if f_off1.file().as_raw_fd() == f_off2.file().as_raw_fd() {
                    let s1 = f_off1.start();
                    let s2 = f_off2.start();
                    let l1 = self.len() as u64;
                    let l2 = other.len() as u64;

                    if s1 < s2 {
                        return s1 + l1 > s2;
                    } else {
                        return s2 + l2 > s1;
                    }
                }
            }
        }
        false
    }

    /// Set the hugetlbfs of the region
    pub fn set_hugetlbfs(&mut self, hugetlbfs: bool) {
        self.hugetlbfs = Some(hugetlbfs)
    }

    /// Returns `true` if the region is hugetlbfs
    pub fn is_hugetlbfs(&self) -> Option<bool> {
        self.hugetlbfs
    }

    /// Returns a reference to the inner bitmap object.
    pub fn bitmap(&self) -> &B {
        &self.bitmap
    }

    /// Returns xen mmap flags.
    pub fn xen_mmap_flags(&self) -> u32 {
        self.mmap.flags()
    }

    /// Returns xen mmap data.
    pub fn xen_mmap_data(&self) -> u32 {
        self.mmap.data()
    }
}

impl<B: Bitmap> VolatileMemory for MmapRegion<B> {
    type B = B;

    fn len(&self) -> usize {
        self.size
    }

    fn get_slice(
        &self,
        offset: usize,
        count: usize,
    ) -> volatile_memory::Result<VolatileSlice<BS<B>>> {
        let _ = self.compute_end_offset(offset, count)?;

        let mmap_info = if self.mmap.mmap_in_advance() {
            None
        } else {
            Some(&self.mmap)
        };

        Ok(
            // SAFETY: Safe because we checked that offset + count was within our range and we only
            // ever hand out volatile accessors.
            unsafe {
                VolatileSlice::with_bitmap(
                    self.as_ptr().add(offset),
                    count,
                    self.bitmap.slice_at(offset),
                    mmap_info,
                )
            },
        )
    }
}

#[derive(Clone, Debug, PartialEq)]
struct MmapUnix {
    addr: *mut u8,
    size: usize,
}

impl MmapUnix {
    fn new(size: usize, prot: i32, flags: i32, fd: i32, f_offset: u64) -> Result<Self> {
        let addr =
        // SAFETY: This is safe because we're not allowing MAP_FIXED, and invalid parameters
        // cannot break Rust safety guarantees (things may change if we're mapping /dev/mem or
        // some wacky file).
            unsafe { libc::mmap(null_mut(), size, prot, flags, fd, f_offset as libc::off_t) };

        if addr == libc::MAP_FAILED {
            return Err(Error::Mmap(io::Error::last_os_error()));
        }

        Ok(Self {
            addr: addr as *mut u8,
            size,
        })
    }

    fn addr(&self) -> *mut u8 {
        self.addr
    }
}

impl Drop for MmapUnix {
    fn drop(&mut self) {
        // SAFETY: This is safe because we mmap the area at addr ourselves, and nobody
        // else is holding a reference to it.
        unsafe {
            libc::munmap(self.addr as *mut libc::c_void, self.size);
        }
    }
}

// Bit mask for the vhost-user xen mmap message.
bitflags! {
    /// Flags for the Xen mmap message.
    pub struct MmapXenFlags: u32 {
        /// Standard Unix memory mapping.
        const UNIX = 0x0;
        /// Xen foreign memory (accessed via /dev/privcmd).
        const FOREIGN = 0x1;
        /// Xen grant memory (accessed via /dev/gntdev).
        const GRANT = 0x2;
        /// Xen no advance mapping.
        const NO_ADVANCE_MAP = 0x8;
        /// All valid mappings.
        const ALL = Self::FOREIGN.bits() | Self::GRANT.bits();
    }
}

impl MmapXenFlags {
    /// Mmap flags are valid.
    pub fn is_valid(&self) -> bool {
        // only one of unix, foreign or grant should be set and mmap_in_advance() should be true
        // with foreign and unix.
        if self.is_grant() {
            !self.is_foreign()
        } else if self.is_foreign() || self.is_unix() {
            self.mmap_in_advance()
        } else {
            false
        }
    }

    /// Is standard Unix memory.
    pub fn is_unix(&self) -> bool {
        self.bits() == Self::UNIX.bits()
    }

    /// Is xen foreign memory.
    pub fn is_foreign(&self) -> bool {
        self.contains(Self::FOREIGN)
    }

    /// Is xen grant memory.
    pub fn is_grant(&self) -> bool {
        self.contains(Self::GRANT)
    }

    /// Can mmap entire region in advance.
    pub fn mmap_in_advance(&self) -> bool {
        !self.contains(Self::NO_ADVANCE_MAP)
    }
}

fn page_size() -> u64 {
    // SAFETY: Safe because this call just returns the page size and doesn't have any side effects.
    unsafe { libc::sysconf(_SC_PAGESIZE) as u64 }
}

fn pages(size: usize) -> (usize, usize) {
    let page_size = page_size() as usize;
    let num = (size + page_size - 1) / page_size;

    (num, page_size * num)
}

fn validate_file(file_offset: &Option<FileOffset>) -> Result<(i32, u64)> {
    let file_offset = match file_offset {
        Some(f) => f,
        None => return Err(Error::InvalidFileOffset),
    };

    let fd = file_offset.file().as_raw_fd();
    let f_offset = file_offset.start();

    // We don't allow file offsets with Xen foreign mappings.
    if f_offset != 0 {
        return Err(Error::InvalidOffsetLength);
    }

    Ok((fd, f_offset))
}

// Xen Foreign memory mapping interface.
trait MmapXenTrait: std::fmt::Debug {
    fn mmap_slice(&self, addr: *const u8, prot: i32, len: usize) -> Result<MmapXenSlice>;
    fn addr(&self) -> *mut u8;
}

// Standard Unix memory mapping for testing other crates.
#[derive(Clone, Debug, PartialEq)]
struct MmapXenUnix(MmapUnix);

impl MmapXenUnix {
    fn new(range: &MmapRange) -> Result<Self> {
        let (fd, offset) = if let Some(ref f_off) = range.file_offset {
            check_file_offset(f_off, range.size)?;
            (f_off.file().as_raw_fd(), f_off.start())
        } else {
            (-1, 0)
        };

        Ok(Self(MmapUnix::new(
            range.size,
            range.prot.ok_or(Error::UnexpectedError)?,
            range.flags.ok_or(Error::UnexpectedError)?,
            fd,
            offset,
        )?))
    }
}

impl MmapXenTrait for MmapXenUnix {
    #[allow(unused_variables)]
    fn mmap_slice(&self, addr: *const u8, prot: i32, len: usize) -> Result<MmapXenSlice> {
        Err(Error::MappedInAdvance)
    }

    fn addr(&self) -> *mut u8 {
        self.0.addr()
    }
}

// Privcmd mmap batch v2 command
//
// include/uapi/xen/privcmd.h: `privcmd_mmapbatch_v2`
#[repr(C)]
#[derive(Debug, Copy, Clone)]
struct PrivCmdMmapBatchV2 {
    // number of pages to populate
    num: u32,
    // target domain
    domid: u16,
    // virtual address
    addr: *mut c_void,
    // array of mfns
    arr: *const u64,
    // array of error codes
    err: *mut c_int,
}

const XEN_PRIVCMD_TYPE: u32 = 'P' as u32;

// #define IOCTL_PRIVCMD_MMAPBATCH_V2 _IOC(_IOC_NONE, 'P', 4, sizeof(privcmd_mmapbatch_v2_t))
fn ioctl_privcmd_mmapbatch_v2() -> c_ulong {
    ioctl_expr(
        _IOC_NONE,
        XEN_PRIVCMD_TYPE,
        4,
        size_of::<PrivCmdMmapBatchV2>() as u32,
    )
}

// Xen foreign memory specific implementation.
#[derive(Clone, Debug, PartialEq)]
struct MmapXenForeign {
    domid: u32,
    guest_base: GuestAddress,
    unix_mmap: MmapUnix,
    fd: i32,
}

impl AsRawFd for MmapXenForeign {
    fn as_raw_fd(&self) -> i32 {
        self.fd
    }
}

impl MmapXenForeign {
    fn new(range: &MmapRange) -> Result<Self> {
        let (fd, f_offset) = validate_file(&range.file_offset)?;
        let (count, size) = pages(range.size);

        let unix_mmap = MmapUnix::new(
            size,
            range.prot.ok_or(Error::UnexpectedError)?,
            range.flags.ok_or(Error::UnexpectedError)? | MAP_SHARED,
            fd,
            f_offset,
        )?;

        let foreign = Self {
            domid: range.mmap_data,
            guest_base: range.addr,
            unix_mmap,
            fd,
        };

        foreign.mmap_ioctl(count)?;
        Ok(foreign)
    }

    // Ioctl to pass additional information to mmap infrastructure of privcmd driver.
    fn mmap_ioctl(&self, count: usize) -> Result<()> {
        let base = self.guest_base.0 / page_size();

        let mut pfn = Vec::with_capacity(count);
        for i in 0..count {
            pfn.push(base + i as u64);
        }

        let mut err: Vec<c_int> = vec![0; count];

        let map = PrivCmdMmapBatchV2 {
            num: count as u32,
            domid: self.domid as u16,
            addr: self.addr() as *mut c_void,
            arr: pfn.as_ptr(),
            err: err.as_mut_ptr(),
        };

        // SAFETY: This is safe because the ioctl guarantees to not access memory beyond `map`.
        let ret = unsafe { ioctl_with_ref(self, ioctl_privcmd_mmapbatch_v2(), &map) };

        if ret == 0 {
            Ok(())
        } else {
            Err(Error::Mmap(io::Error::last_os_error()))
        }
    }
}

impl MmapXenTrait for MmapXenForeign {
    #[allow(unused_variables)]
    fn mmap_slice(&self, addr: *const u8, prot: i32, len: usize) -> Result<MmapXenSlice> {
        Err(Error::MappedInAdvance)
    }

    fn addr(&self) -> *mut u8 {
        self.unix_mmap.addr()
    }
}

// Xen Grant memory mapping interface.

const XEN_GRANT_ADDR_OFF: u64 = 1 << 63;

// Grant reference
//
// include/uapi/xen/gntdev.h: `ioctl_gntdev_grant_ref`
#[repr(C)]
#[derive(Copy, Clone, Debug, Default, PartialEq)]
struct GntDevGrantRef {
    // The domain ID of the grant to be mapped.
    domid: u32,
    // The grant reference of the grant to be mapped.
    reference: u32,
}

#[repr(C)]
#[derive(Debug, Default, PartialEq, Eq)]
struct __IncompleteArrayField<T>(::std::marker::PhantomData<T>, [T; 0]);
impl<T> __IncompleteArrayField<T> {
    #[inline]
    unsafe fn as_ptr(&self) -> *const T {
        self as *const __IncompleteArrayField<T> as *const T
    }
    #[inline]
    unsafe fn as_mut_ptr(&mut self) -> *mut T {
        self as *mut __IncompleteArrayField<T> as *mut T
    }
    #[inline]
    unsafe fn as_slice(&self, len: usize) -> &[T] {
        ::std::slice::from_raw_parts(self.as_ptr(), len)
    }
    #[inline]
    unsafe fn as_mut_slice(&mut self, len: usize) -> &mut [T] {
        ::std::slice::from_raw_parts_mut(self.as_mut_ptr(), len)
    }
}

// Grant dev mapping reference
//
// include/uapi/xen/gntdev.h: `ioctl_gntdev_map_grant_ref`
#[repr(C)]
#[derive(Debug, Default)]
struct GntDevMapGrantRef {
    // The number of grants to be mapped.
    count: u32,
    // Unused padding
    pad: u32,
    // The offset to be used on a subsequent call to mmap().
    index: u64,
    // Array of grant references, of size @count.
    refs: __IncompleteArrayField<GntDevGrantRef>,
}

generate_fam_struct_impl!(
    GntDevMapGrantRef,
    GntDevGrantRef,
    refs,
    u32,
    count,
    usize::MAX
);

type GntDevMapGrantRefWrapper = FamStructWrapper<GntDevMapGrantRef>;

impl GntDevMapGrantRef {
    fn new(domid: u32, base: u32, count: usize) -> Result<GntDevMapGrantRefWrapper> {
        let mut wrapper = GntDevMapGrantRefWrapper::new(count).map_err(Error::Fam)?;
        let refs = wrapper.as_mut_slice();

        // GntDevMapGrantRef's pad and index are initialized to 0 by Fam layer.
        for (i, r) in refs.iter_mut().enumerate().take(count) {
            r.domid = domid;
            r.reference = base + i as u32;
        }

        Ok(wrapper)
    }
}

// Grant dev un-mapping reference
//
// include/uapi/xen/gntdev.h: `ioctl_gntdev_unmap_grant_ref`
#[repr(C)]
#[derive(Debug, Copy, Clone)]
struct GntDevUnmapGrantRef {
    // The offset returned by the map operation.
    index: u64,
    // The number of grants to be unmapped.
    count: u32,
    // Unused padding
    pad: u32,
}

impl GntDevUnmapGrantRef {
    fn new(index: u64, count: u32) -> Self {
        Self {
            index,
            count,
            pad: 0,
        }
    }
}

const XEN_GNTDEV_TYPE: u32 = 'G' as u32;

// #define IOCTL_GNTDEV_MAP_GRANT_REF _IOC(_IOC_NONE, 'G', 0, sizeof(ioctl_gntdev_map_grant_ref))
fn ioctl_gntdev_map_grant_ref() -> c_ulong {
    ioctl_expr(
        _IOC_NONE,
        XEN_GNTDEV_TYPE,
        0,
        (size_of::<GntDevMapGrantRef>() + size_of::<GntDevGrantRef>()) as u32,
    )
}

// #define IOCTL_GNTDEV_UNMAP_GRANT_REF _IOC(_IOC_NONE, 'G', 1, sizeof(struct ioctl_gntdev_unmap_grant_ref))
fn ioctl_gntdev_unmap_grant_ref() -> c_ulong {
    ioctl_expr(
        _IOC_NONE,
        XEN_GNTDEV_TYPE,
        1,
        size_of::<GntDevUnmapGrantRef>() as u32,
    )
}

// Xen grant memory specific implementation.
#[derive(Clone, Debug)]
struct MmapXenGrant {
    guest_base: GuestAddress,
    unix_mmap: Option<MmapUnix>,
    file_offset: FileOffset,
    flags: i32,
    size: usize,
    index: u64,
    domid: u32,
}

impl AsRawFd for MmapXenGrant {
    fn as_raw_fd(&self) -> i32 {
        self.file_offset.file().as_raw_fd()
    }
}

impl MmapXenGrant {
    fn new(range: &MmapRange, mmap_flags: MmapXenFlags) -> Result<Self> {
        validate_file(&range.file_offset)?;

        let mut grant = Self {
            guest_base: range.addr,
            unix_mmap: None,
            file_offset: range.file_offset.as_ref().unwrap().clone(),
            flags: range.flags.ok_or(Error::UnexpectedError)?,
            size: 0,
            index: 0,
            domid: range.mmap_data,
        };

        // Region can't be mapped in advance, partial mapping will be done later via
        // `MmapXenSlice`.
        if mmap_flags.mmap_in_advance() {
            let (unix_mmap, index) = grant.mmap_range(
                range.addr,
                range.size,
                range.prot.ok_or(Error::UnexpectedError)?,
            )?;

            grant.unix_mmap = Some(unix_mmap);
            grant.index = index;
            grant.size = range.size;
        }

        Ok(grant)
    }

    fn mmap_range(&self, addr: GuestAddress, size: usize, prot: i32) -> Result<(MmapUnix, u64)> {
        let (count, size) = pages(size);
        let index = self.mmap_ioctl(addr, count)?;
        let unix_mmap = MmapUnix::new(size, prot, self.flags, self.as_raw_fd(), index)?;

        Ok((unix_mmap, index))
    }

    fn unmap_range(&self, unix_mmap: MmapUnix, size: usize, index: u64) {
        let (count, _) = pages(size);

        // Unmap the address first.
        drop(unix_mmap);
        self.unmap_ioctl(count as u32, index).unwrap();
    }

    fn mmap_ioctl(&self, addr: GuestAddress, count: usize) -> Result<u64> {
        let base = ((addr.0 & !XEN_GRANT_ADDR_OFF) / page_size()) as u32;
        let wrapper = GntDevMapGrantRef::new(self.domid, base, count)?;
        let reference = wrapper.as_fam_struct_ref();

        // SAFETY: This is safe because the ioctl guarantees to not access memory beyond reference.
        let ret = unsafe { ioctl_with_ref(self, ioctl_gntdev_map_grant_ref(), reference) };

        if ret == 0 {
            Ok(reference.index)
        } else {
            Err(Error::Mmap(io::Error::last_os_error()))
        }
    }

    fn unmap_ioctl(&self, count: u32, index: u64) -> Result<()> {
        let unmap = GntDevUnmapGrantRef::new(index, count);

        // SAFETY: This is safe because the ioctl guarantees to not access memory beyond unmap.
        let ret = unsafe { ioctl_with_ref(self, ioctl_gntdev_unmap_grant_ref(), &unmap) };

        if ret == 0 {
            Ok(())
        } else {
            Err(Error::Mmap(io::Error::last_os_error()))
        }
    }
}

impl MmapXenTrait for MmapXenGrant {
    // Maps a slice out of the entire region.
    fn mmap_slice(&self, addr: *const u8, prot: i32, len: usize) -> Result<MmapXenSlice> {
        MmapXenSlice::new_with(self.clone(), addr as usize, prot, len)
    }

    fn addr(&self) -> *mut u8 {
        if let Some(ref unix_mmap) = self.unix_mmap {
            unix_mmap.addr()
        } else {
            null_mut()
        }
    }
}

impl Drop for MmapXenGrant {
    fn drop(&mut self) {
        if let Some(unix_mmap) = self.unix_mmap.take() {
            self.unmap_range(unix_mmap, self.size, self.index);
        }
    }
}

#[derive(Debug)]
pub(crate) struct MmapXenSlice {
    grant: Option<MmapXenGrant>,
    unix_mmap: Option<MmapUnix>,
    addr: *mut u8,
    size: usize,
    index: u64,
}

impl MmapXenSlice {
    fn raw(addr: *mut u8) -> Self {
        Self {
            grant: None,
            unix_mmap: None,
            addr,
            size: 0,
            index: 0,
        }
    }

    fn new_with(grant: MmapXenGrant, offset: usize, prot: i32, size: usize) -> Result<Self> {
        let page_size = page_size() as usize;
        let page_base: usize = (offset / page_size) * page_size;
        let offset = offset - page_base;
        let size = offset + size;

        let addr = grant.guest_base.0 + page_base as u64;
        let (unix_mmap, index) = grant.mmap_range(GuestAddress(addr), size, prot)?;

        // SAFETY: We have already mapped the range including offset.
        let addr = unsafe { unix_mmap.addr().add(offset) };

        Ok(Self {
            grant: Some(grant),
            unix_mmap: Some(unix_mmap),
            addr,
            size,
            index,
        })
    }

    // Mapped address for the region.
    pub(crate) fn addr(&self) -> *mut u8 {
        self.addr
    }
}

impl Drop for MmapXenSlice {
    fn drop(&mut self) {
        // Unmaps memory automatically once this instance goes out of scope.
        if let Some(unix_mmap) = self.unix_mmap.take() {
            self.grant
                .as_ref()
                .unwrap()
                .unmap_range(unix_mmap, self.size, self.index);
        }
    }
}

#[derive(Debug)]
pub struct MmapXen {
    xen_flags: MmapXenFlags,
    domid: u32,
    mmap: Box<dyn MmapXenTrait>,
}

impl MmapXen {
    fn new(range: &MmapRange) -> Result<Self> {
        let xen_flags = match MmapXenFlags::from_bits(range.mmap_flags) {
            Some(flags) => flags,
            None => return Err(Error::MmapFlags(range.mmap_flags)),
        };

        if !xen_flags.is_valid() {
            return Err(Error::MmapFlags(xen_flags.bits()));
        }

        Ok(Self {
            xen_flags,
            domid: range.mmap_data,
            mmap: if xen_flags.is_foreign() {
                Box::new(MmapXenForeign::new(range)?)
            } else if xen_flags.is_grant() {
                Box::new(MmapXenGrant::new(range, xen_flags)?)
            } else {
                Box::new(MmapXenUnix::new(range)?)
            },
        })
    }

    fn addr(&self) -> *mut u8 {
        self.mmap.addr()
    }

    fn flags(&self) -> u32 {
        self.xen_flags.bits()
    }

    fn data(&self) -> u32 {
        self.domid
    }

    fn mmap_in_advance(&self) -> bool {
        self.xen_flags.mmap_in_advance()
    }

    pub(crate) fn mmap(
        mmap_xen: Option<&Self>,
        addr: *mut u8,
        prot: i32,
        len: usize,
    ) -> MmapXenSlice {
        match mmap_xen {
            Some(mmap_xen) => mmap_xen.mmap.mmap_slice(addr, prot, len).unwrap(),
            None => MmapXenSlice::raw(addr),
        }
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::undocumented_unsafe_blocks)]

    use super::*;
    use vmm_sys_util::tempfile::TempFile;

    // Adding a helper method to extract the errno within an Error::Mmap(e), or return a
    // distinctive value when the error is represented by another variant.
    impl Error {
        fn raw_os_error(&self) -> i32 {
            match self {
                Error::Mmap(e) => e.raw_os_error().unwrap(),
                _ => std::i32::MIN,
            }
        }
    }

    #[allow(unused_variables)]
    pub unsafe fn ioctl_with_ref<F: AsRawFd, T>(fd: &F, req: c_ulong, arg: &T) -> c_int {
        0
    }

    impl MmapRange {
        fn initialized(is_file: bool) -> Self {
            let file_offset = if is_file {
                Some(FileOffset::new(TempFile::new().unwrap().into_file(), 0))
            } else {
                None
            };

            let mut range = MmapRange::new_unix(0x1000, file_offset, GuestAddress(0x1000));
            range.prot = Some(libc::PROT_READ | libc::PROT_WRITE);
            range.mmap_data = 1;

            range
        }
    }

    impl MmapRegion {
        pub fn new(size: usize) -> Result<Self> {
            let range = MmapRange::new_unix(size, None, GuestAddress(0));
            Self::from_range(range)
        }
    }

    #[test]
    fn test_mmap_xen_failures() {
        let mut range = MmapRange::initialized(true);
        // Invalid flags
        range.mmap_flags = 16;

        let r = MmapXen::new(&range);
        assert_eq!(
            format!("{:?}", r.unwrap_err()),
            format!("MmapFlags({})", range.mmap_flags),
        );

        range.mmap_flags = MmapXenFlags::FOREIGN.bits() | MmapXenFlags::GRANT.bits();
        let r = MmapXen::new(&range);
        assert_eq!(
            format!("{:?}", r.unwrap_err()),
            format!("MmapFlags({:x})", MmapXenFlags::ALL.bits()),
        );

        range.mmap_flags = MmapXenFlags::FOREIGN.bits() | MmapXenFlags::NO_ADVANCE_MAP.bits();
        let r = MmapXen::new(&range);
        assert_eq!(
            format!("{:?}", r.unwrap_err()),
            format!(
                "MmapFlags({:x})",
                MmapXenFlags::NO_ADVANCE_MAP.bits() | MmapXenFlags::FOREIGN.bits(),
            ),
        );
    }

    #[test]
    fn test_mmap_xen_success() {
        let mut range = MmapRange::initialized(true);
        range.mmap_flags = MmapXenFlags::FOREIGN.bits();

        let r = MmapXen::new(&range).unwrap();
        assert_eq!(r.flags(), range.mmap_flags);
        assert_eq!(r.data(), range.mmap_data);
        assert_ne!(r.addr(), null_mut());
        assert!(r.mmap_in_advance());

        range.mmap_flags = MmapXenFlags::GRANT.bits();
        let r = MmapXen::new(&range).unwrap();
        assert_eq!(r.flags(), range.mmap_flags);
        assert_eq!(r.data(), range.mmap_data);
        assert_ne!(r.addr(), null_mut());
        assert!(r.mmap_in_advance());

        range.mmap_flags = MmapXenFlags::GRANT.bits() | MmapXenFlags::NO_ADVANCE_MAP.bits();
        let r = MmapXen::new(&range).unwrap();
        assert_eq!(r.flags(), range.mmap_flags);
        assert_eq!(r.data(), range.mmap_data);
        assert_eq!(r.addr(), null_mut());
        assert!(!r.mmap_in_advance());
    }

    #[test]
    fn test_foreign_map_failure() {
        let mut range = MmapRange::initialized(true);
        range.file_offset = Some(FileOffset::new(TempFile::new().unwrap().into_file(), 0));
        range.prot = None;
        let r = MmapXenForeign::new(&range);
        assert_eq!(format!("{:?}", r.unwrap_err()), "UnexpectedError");

        let mut range = MmapRange::initialized(true);
        range.flags = None;
        let r = MmapXenForeign::new(&range);
        assert_eq!(format!("{:?}", r.unwrap_err()), "UnexpectedError");

        let mut range = MmapRange::initialized(true);
        range.file_offset = Some(FileOffset::new(TempFile::new().unwrap().into_file(), 1));
        let r = MmapXenForeign::new(&range);
        assert_eq!(format!("{:?}", r.unwrap_err()), "InvalidOffsetLength");

        let mut range = MmapRange::initialized(true);
        range.size = 0;
        let r = MmapXenForeign::new(&range);
        assert_eq!(r.unwrap_err().raw_os_error(), libc::EINVAL);
    }

    #[test]
    fn test_foreign_map_success() {
        let range = MmapRange::initialized(true);
        let r = MmapXenForeign::new(&range).unwrap();
        assert_ne!(r.addr(), null_mut());
        assert_eq!(r.domid, range.mmap_data);
        assert_eq!(r.guest_base, range.addr);
    }

    #[test]
    fn test_grant_map_failure() {
        let mut range = MmapRange::initialized(true);
        range.prot = None;
        let r = MmapXenGrant::new(&range, MmapXenFlags::empty());
        assert_eq!(format!("{:?}", r.unwrap_err()), "UnexpectedError");

        let mut range = MmapRange::initialized(true);
        range.prot = None;
        // Protection isn't used for no-advance mappings
        MmapXenGrant::new(&range, MmapXenFlags::NO_ADVANCE_MAP).unwrap();

        let mut range = MmapRange::initialized(true);
        range.flags = None;
        let r = MmapXenGrant::new(&range, MmapXenFlags::NO_ADVANCE_MAP);
        assert_eq!(format!("{:?}", r.unwrap_err()), "UnexpectedError");

        let mut range = MmapRange::initialized(true);
        range.file_offset = Some(FileOffset::new(TempFile::new().unwrap().into_file(), 1));
        let r = MmapXenGrant::new(&range, MmapXenFlags::NO_ADVANCE_MAP);
        assert_eq!(format!("{:?}", r.unwrap_err()), "InvalidOffsetLength");

        let mut range = MmapRange::initialized(true);
        range.size = 0;
        let r = MmapXenGrant::new(&range, MmapXenFlags::empty());
        assert_eq!(r.unwrap_err().raw_os_error(), libc::EINVAL);
    }

    #[test]
    fn test_grant_map_success() {
        let range = MmapRange::initialized(true);
        let r = MmapXenGrant::new(&range, MmapXenFlags::NO_ADVANCE_MAP).unwrap();
        assert_eq!(r.addr(), null_mut());
        assert_eq!(r.domid, range.mmap_data);
        assert_eq!(r.guest_base, range.addr);

        let mut range = MmapRange::initialized(true);
        // Size isn't used with no-advance mapping.
        range.size = 0;
        MmapXenGrant::new(&range, MmapXenFlags::NO_ADVANCE_MAP).unwrap();

        let range = MmapRange::initialized(true);
        let r = MmapXenGrant::new(&range, MmapXenFlags::empty()).unwrap();
        assert_ne!(r.addr(), null_mut());
        assert_eq!(r.domid, range.mmap_data);
        assert_eq!(r.guest_base, range.addr);
    }

    #[test]
    fn test_grant_ref_alloc() {
        let wrapper = GntDevMapGrantRef::new(0, 0x1000, 0x100).unwrap();
        let r = wrapper.as_fam_struct_ref();
        assert_eq!(r.count, 0x100);
        assert_eq!(r.pad, 0);
        assert_eq!(r.index, 0);
    }
}
